Project 02: The Effects of Initial Velocity Angle and Nucleic Charge on Saturnian Orbital Trajectories¶
Date: October 20, 2025
Name: Rhea Zhu
Table of Contents¶
- 2. Analysis: Combinations of Initial Velocity Angle and N₂ Charge
Appendix: Code Validation¶
Appendix: Reflection Questions¶
Overview¶
In this project, I use Solve_IVP to investigate the conditions needed to achieve stable Saturnian trajectories for an electron orbiting two nuclei of variable charge. Specifically, I vary the electron's initial velocity angle and distance and ask the following:
Research Questions
- What electron path trajectories are traced when varying (1) initial velocity angle, and (2) nucleic charge?
- What combinations of initial velocity angle and nucleic charge create stable orbital trajectories?
Experimental Method & Analysis¶
- Setup: Two nuclei, call $N_1$ and $N_2$, are vertically placed $1.2 \times 10^{-10}m$ apart. An electron $e$ is placed $5.29 \times 10^{-11}m$ from the point bisecting $N_1$ and $N_2$. $N_1$ has a fixed nucleic charge of $-e^- = 1.6 \times 10^{-19} C$, while $N_2$ is variable
- Initial Parameters: In this simulation, two parameters are explored: initial electron velocity and $N_2$ nucleic charge:
- Initial electron velocity angle: In this experiment, we restrict the domain of $\theta$ to $0 \leq \theta \leq 180^o$. We choose to restrict $\theta$ to two quadrants, since we expect the electron trajectories of domain $\theta$ to $0 \leq \theta \leq 180^o$ to be symmetrical to $0 \leq \theta \leq -180^o$.
- $N_2$ nucleic charge: Since $N_2$ is a nucleus, its charge is restricted to positive values. So, the range of values for $N_2$ charge, call $c$, is $(c > 0) C$.
- Start Simulation: When the simulation begins, the electron orbits at a speed of $2.18 \times 10^6 m/s$ around the two nuclei; this speed was determined by an electron's speed around a hydrogen atom a Bohr radius away from the nucleus. The electron's trajectory follows the Coulomb force [1]: $$F_{orbit} = \frac{q_1 q_2}{4 \pi \epsilon_0 r^2} \vec{r}$$ However, since the electron is in the presence of two nuclei, its resultant force is the vector sum of both Coulomb forces. So, if we let $c$ be the charge of $N_2$. then $$F_{total} = \frac{e^- c}{4 \pi \epsilon_0 r_{N_2^2}} \vec{r_{N_2}} + \frac{-e^2}{4 \pi \epsilon_0 r_{N_1^2}} \vec{r_{N_1}}$$ The electron orbits for $5$ seconds before the simulation is stopped.
- Outcome: By varying the initial parameters, we analyze what parametric combinations create stable and unstable orbits. Note that stable orbits are periodic and predictable, whereas unstable orbits are random and chaotic.
Note that we define $\theta$ as the angle the velocity vector makes with the horizontal x-axis; that is, $90^o$ corresponds to the vector parallel to the line defined by $N_1$ and $N_2$
Defining a stable orbit¶
We quantitatively define a stable orbit in two ways:
Periodicity: The relative standard deviation between all periods is within 10%. For each time step during a simulation run, the distance between the electron’s current position and its original position is calculated, and the local minima of these distances are found; the time intervals between the local minima represent approximate “periods” of the electron’s trajectory. Using this, the relative standard deviation between the time intervals is calculated; if it is less than 10%, the orbit is deemed periodic.
Electron-proton collision: At any time step, if he electron is within 8.5e-16m of either proton, the orbit is unstable. Note that 8.5e-16 m was chosen, since it is the radius of the proton. Therefore, if the electron’s position is within its radius, it is assumed to have collided with the proton.
If the electron trajectory is both periodic and does not collide with the proton, we say that the orbit is stable.
Programming Analysis¶
Programming with Solve_IVP [2]¶
In this project, we will use the function Solve_IVP to simulate the electron's path trajectories. Solve_IVP is a function in Python's SciPy library, designed to solve ordinary differential equations (ODEs) in initial value problems (IVPs). To do this, Solve_IVP takes initial variables (such as position and velocity) and calls a function specified by the user to calculate the derivatives of those initial states; then, Solve_IVP takes these equations to calculate position and velocity at each specified time step, using a technique similar to Euler's method.
This is useful for the simulation, since we are modelling the components for position $x, y$, velocity $v_x, v_y$, acceleration $a_x, a_y$, and force $F_x, F_y$, where $$F_x = ma_x, a_x = \frac{dv_x}{dt}, v_x = \frac{dx}{dt}$$ and $$F_y = ma_y, a_y = \frac{dv_y}{dt}, v_y = \frac{dy}{dt}$$
To use Solve_IVP, we first define the initial state of our electron, with the tuple $(x_0, y_0, v_x0, v_y0)$. We then create a function specifying the differential relationships between these four values. Using the equation for Coulomb force: $$F = \frac{q_1q_2}{4 \pi r^2}$$ We can calculate the component forces on the electron at each time step, then use that to calculate acceleration. The derivative for $x_0$ and $y_0$ woudl then be $v_{x0}$ and $v_{y0}$ respectively, and the derivatives for $v_{x0}$ and $v_{y0}$ are $v_{a0}$ and $a_{y0}$. We return these four values from our function, which Solve_IVP then uses to calculate the trajectory of the electron.
We execute each simulation over a time span of $1e-14$s. This value was determined via several initial simulation tests over a large range of nucleic charge and initial velocity angle combinations; it was found that 1e-14s was the most appropriate period that both (1) accurately represents the full trajectory of the electrons, and (2) is not too long as to overly prolong the code runtime.
Analysis Methods¶
This project's analysis will be broken into four portions.
- Preliminary Trial Analysis: In this section, we manually alter the $N_2$ and $\theta$ parameters and qualitatively observe the trajectories of the electrons as they shift. By conducting these preliminary trials, we gain general knowledge of how nucleic charge and initial velocity angle affect electron orbital trajectories. Note that stability is not tested here, since we want to first get a preliminary understanding of the effects of nucleic charge and angles on electron orbital paths.
- Discrete Longitudinal Phase Space Analysis: In this section, we quantitatively determine if orbits are stable over a wide range in our phase space by running our simulation with $\theta$ and $N_2$ combinations throughout our domains of $0 \leq \theta \leq 90^o$ and $\frac{1}{100}N_1\leq N_2 \leq 100N_1$. We choose the $N_2$ range based on the maximum value of $N_2$ tested in the preliminary trials, which is 100. We take the inverse of 100 for the lower bound of our $N_2$ domain. We also choose our $\theta$ to be restricted to our first quadrant to limit the program's run time; from preliminary trial analysis, the behavior of obtuse angles is very similar to the behavior of acute angles, so we do not expect this restriction to compromise our project's validity.The discrete phase space analysis outputs which combinations of initial velocity angel and $\frac{N_2}{N_1}$ parameters leads to (1) stable, (2) unstable/quasi-periodic, and (3) collision orbits
- Continuous Longitudinal Phase Space Analysis: In Part III, we extend about our discrete phase space by running a continuous phase space analysis on the relative standard deviation of each combination of our phase space parameters; we also increase the resolution of our phase space to more deeply understand the patterns seen in our simulations. This portion of our analysis goes deeper into the justification of stable, unstable, and collision orbits, and alo checks the validity of our quantification of stability.
- Numerical Artifacts and Quantification of stability: In this final section, we look at the numerical artifacts present in our continuous longitudinal phase space graph and try to understand why such artifacts are present. We reflect on these results and connect them to possible limitations to our quantification of stability.
Physical Assumptions¶
The following assumptions are made in our simulation:
- Two Exclusive Forces: The only two forces acting on the electron are those of $N_1$ and $N_2$. We neglect the gravitational forces the electron experiences from $N_1$ and $N_2$, since they are magnitudes weaker than electrostatic force.
- Classical Interpretation: The simulation assumes an entirely classical interpretation of the setup, using Coulomb force as the main driver of this project.
- Neglect Relativistic Effects: Since the electron's velocity is magnitudes smaller than the speed of light, we can also reasonably neglect relativistic effects.
- Point-Like: We treat the three particles as point-like figures.
Analysis: Investigating Combinations of Initial Velocity Angle and N2 Charge¶
Part I: Preliminary Trial Analysis To Understand Orbital Trajectories¶
In Part I, we manually choose various combinations of $N_2$ and $\theta$ to run the simulation, observing the qualitative characteristics of the electron trajectory.
For this portion of analysis, we separate it into four cycles. We firstly set $N_2 = N_1$ and alter $\theta$. We then simulate electron trajectories when $N_2$ is larger, that is, when $N_2 = 5N_1$, and also alter $\theta$. For cycle 3, we do the opposite and set $N_2 = \frac{1}{5}N_1$, and for cycle 4, we let $N_2$ be much larger than $N_1$ where $N_2 = 100N_1$. By picking various ratios of $N_2$ and $N_1$, we take a very broad range of data to capture most of the possible electron path trajectories in the simulation.
Note that the $\theta$ we pick to alter for each cylce was determined after initial testing, where each $\theta$ analyzed below shows a unique pattern of characteristics.
Cycle 1: $N_2 = N_1$, Altering $\theta$¶
For our first cycle, we let $N_2 = N_1$, and set $\theta = 5^o, 20^o, 90^o, 120^o$.
Trial 1: $\theta = 5^o$¶
# SIMULATION CODE
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
import math
# Constants
k = 8.99e9 # Coulomb constant, N·m²/C²
e = -1.6e-19 # Electron charge magnitude, C
n1 = 1.6e-19 # Nucleus 1 charge
me = 9.109e-31 # Electron mass, kg
r0 = 5.29e-11 # Bohr radius, m
distance = 1.2 * 10**(-10) #vertical distance between two nuclei, m
y1 = distance / 2 # N1 y coordinate, m
x1 = 0 # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0 # N2 y coordinate, m
def get_trajectory(n2, angle, T):
# Initial Velocity
v0 = 2.18e6 # initial velocity (m/s)
theta = math.radians(angle) #convert angle to radians, rad
vx0 = math.cos(theta) * v0 # initial x velocity (m/s)
vy0 = math.sin(theta) * v0 # initial y velocity (m/s)
state0 = (r0, 0, vx0, vy0) # (x0, y0, vx0, vy0) (m,m,m/s,m/s)
t_span = (0, T) # span for simulation to run
# Nuclei coordinates
# We define the coordinate (0,0) to be the halfway point between N1 and N2
distance = 1.2 * 10**(-10) #vertical distance between two nuclei, m
y1 = distance / 2 # N1 y coordinate, m
x1 = 0 # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0 # N2 y coordinate, m
# Get your solution from solve_ivp
sol1 = solve_ivp(diff_eqns, t_span, state0, rtol=1e-9, atol=1e-9)
print(sol1)
return sol1
# Define your diff_eqns function
def diff_eqns(t, state):
x, y, vx, vy = state #state variables in timestep, m, m, m/s, m/s
r1x = x - x1 #x distance between electron & N1, m
r1y = y - y1 #y distance between electron & N1, m
r1 = np.sqrt(r1x**2+r1y**2) #distance between N1 and electron
r2x = x - x2 #x distance between electron & N2, m
r2y = y - y2 #y distance between electron & N2, m
r2 = np.sqrt(r2x**2+r2y**2) #distance between N2 and electron
# Calculate the magnitude of the force
# Calculate force & acceleration components for N1
fx1 = k * e * n1 * r1x / r1**3
fy1 = k * e * n1 * r1y / r1**3
# Calculate force & acceleration components for N1
fx2= k * e * n2 * r2x / r2**3
fy2 = k * e * n2 * r2y / r2**3
fx = fx1 + fx2
fy = fy1 + fy2
# Calculate acceleration
accx = fx/me
accy = fy/me
# Return differentials
return vx, vy, accx, accy
def plot_trajectory(sol1, top, T, c):
# Plot y vs x for theta = 5
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 9)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=top)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_xlim(x_min, x_max)
a.set_ylim(y_min, c * y_max)
plt.show()
Code Block Summary: This block creates the functiosn to calculate the trajectory of the electron given n2 and angle, using Solve_IVP and the differential equations function, which models Coulomb force. A function for plotting trajectory is also created.
# Parameters to vary
n2 = 1.6e-19 # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 5e-15 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.25, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.643e-18 ... 5.000e-15 5.000e-15]
y: [[ 5.290e-11 5.640e-11 ... -7.929e-11 -7.959e-11]
[ 0.000e+00 3.123e-13 ... 2.018e-11 2.015e-11]
[ 2.172e+06 2.087e+06 ... -1.509e+06 -1.501e+06]
[ 1.900e+05 1.901e+05 ... -1.270e+05 -1.270e+05]]
sol: None
t_events: None
y_events: None
nfev: 29240
njev: 0
nlu: 0
Figure 2. The electron follows a trajectory first to $N_1$, then loops around to $N_2$. The density of the electron's trajectory is significantly higher towards the $y=0$ axis, and overall traces an ellipsoid-like shape.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 5$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 2: $\theta = 20^o$¶
Next, we investigate electron trajectories when $N_1$ and $N_2$ have equal charges while varying $\theta$.
# Parameters to vary
n2 = 1.6e-19 # Nucleus 2 charge
angle = 20 # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.25, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.673e-18 ... 4.999e-15 5.000e-15]
y: [[ 5.290e-11 5.625e-11 ... 1.017e-10 1.021e-10]
[ 0.000e+00 1.247e-12 ... -4.251e-11 -4.194e-11]
[ 2.049e+06 1.962e+06 ... 4.277e+05 3.965e+05]
[ 7.456e+05 7.462e+05 ... 5.781e+05 5.826e+05]]
sol: None
t_events: None
y_events: None
nfev: 38888
njev: 0
nlu: 0
Figure 3. Similar to $\theta = 20$, electron follows a trajectory first to $N_1$, then loops around to $N_2$. The cycles in the electron's path are much closer together, indicating that the oribit is more periodic.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 20$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 3: $\theta = 90^o$¶
# Parameters to vary
n2 = 1.6e-19 # Nucleus 2 charge
angle = 90 # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.1, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.915e-23 ... 5.000e-15 5.000e-15]
y: [[ 5.290e-11 5.290e-11 ... 8.553e-12 8.175e-12]
[ 0.000e+00 4.174e-17 ... 2.065e-11 2.194e-11]
[ 1.335e-10 -1.000e+00 ... -9.347e+05 -9.499e+05]
[ 2.180e+06 2.180e+06 ... 3.166e+06 3.214e+06]]
sol: None
t_events: None
y_events: None
nfev: 119396
njev: 0
nlu: 0
Figure 4. Again, the electron follows a trajectory first to $N_1$, then loops around to $N_2$. The overall density of the electron's path is larger and the semi-major axis is much higher.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 4: $\theta = 120^o$¶
Finally, we investigate electron trajectories when $N_1$ and $N_2$ have equal charges while varying $\theta$.
# Parameters to vary
n2 = 1.6e-19 # Nucleus 2 charge
angle = 120 # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1,1.2, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.461e-18 ... 5.000e-15 5.000e-15]
y: [[ 5.290e-11 5.125e-11 ... -2.008e-13 -1.409e-13]
[ 0.000e+00 2.759e-12 ... 4.462e-11 4.450e-11]
[-1.090e+06 -1.167e+06 ... 2.397e+06 2.397e+06]
[ 1.888e+06 1.889e+06 ... -4.905e+06 -4.879e+06]]
sol: None
t_events: None
y_events: None
nfev: 68720
njev: 0
nlu: 0
Figure 5. The electron follows a trajectory first to $N_1$, then loops around to $N_2$. Whiel it traces on overall ellptical shape like the others, compared to the other angles, this trajectory's ellipsoid is more spherical.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 120$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Cycle 1 Analysis¶
From cycle 1, we notice the following:
- Ellipsoid: Across all four trials, the electron trajectories trace an ellipsoid pattern, where the electron first loops up towards $N_1$, down towards $N_2$, and back up to $N_1$, and repeats.
- Semi-Major Axis: As the angle nears 90, the semimajor axis of an ellipsoid increases. This is due to the increase in the electron's vertical; since the y-component of electron velocity is highest at 90 degrees, so is the y-distance the electron travels.
- Angle: While the angle does not change the overall shape the electron traces, we see that the "density" of the shape changes. For instance, when $\theta = 20$, the electron paths are much denser and periodic. Furthermore, when $\theta = 5$, we see a density increase along the $y=0$ axis, since the initial horizontal velocity component is large.
Cycle 2: $N_2 = 5N_1$, Altering $\theta$¶
For our second cycle, we let $N_2 = 5N_1$, and we set $\theta = 5^o, 90^o, 135^o$.
Trial 1: $\theta = 5^o$¶
We investigate electron trajectories when $N_2 = 5N_1$ have equal charges and $\theta = 5^o$.
# Parameters to vary
n2 = 5 * 1.6e-19 # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.43, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 9.683e-19 ... 5.000e-15 5.000e-15]
y: [[ 5.290e-11 5.493e-11 ... 5.267e-11 5.269e-11]
[ 0.000e+00 1.294e-13 ... -4.487e-11 -4.387e-11]
[ 2.172e+06 2.021e+06 ... 1.447e+05 4.602e+04]
[ 1.900e+05 7.829e+04 ... 4.152e+06 4.127e+06]]
sol: None
t_events: None
y_events: None
nfev: 107762
njev: 0
nlu: 0
Figure 6. The electron follows a trajectory that solely orbits $N_2$. Due to this, the distance the electron travels each period is much shorter, indicating a smaller period. As a result, the density of the electron's trajectory is high, and overall traces a bowl-like shape.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 5$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 2: $\theta = 90^o$¶
We investigate electron trajectories when $N_2 = 5N_1$ have equal charges and $\theta = 90^0$.
# Parameters to vary
n2 = 5 * 1.6e-19 # Nucleus 2 charge
angle = 90 # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.3, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 6.382e-24 ... 4.999e-15 5.000e-15]
y: [[ 5.290e-11 5.290e-11 ... -4.648e-11 -4.702e-11]
[ 0.000e+00 1.391e-17 ... 2.934e-11 2.995e-11]
[ 1.335e-10 -1.000e+00 ... -6.755e+05 -5.679e+05]
[ 2.180e+06 2.180e+06 ... 7.454e+05 6.893e+05]]
sol: None
t_events: None
y_events: None
nfev: 77594
njev: 0
nlu: 0
Figure 7. The electron follows a trajectory solely orbiting $N_2$. However, due to its initial y-velocity component resulting from $\theta = 90$, the electron reaches closer to $N_1$ before reversing direction towards $N_2$. This results in a partial-ellipsoid like shape.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 3: $\theta = 135^o$¶
Finally, we investigate electron trajectories when $N_2 = 5N_1$ have equal charges and $\theta = 135^0$.
# Parameters to vary
n2 = 5 * 1.6e-19 # Nucleus 2 charge
angle = 135 # Launch angle in degrees
T = 0.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.4, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 9.637e-19 ... 4.999e-15 5.000e-15]
y: [[ 5.290e-11 5.134e-11 ... -6.092e-11 -6.115e-11]
[ 0.000e+00 1.430e-12 ... 9.130e-12 9.742e-12]
[-1.541e+06 -1.691e+06 ... -3.150e+05 -2.016e+05]
[ 1.541e+06 1.427e+06 ... 7.322e+05 6.569e+05]]
sol: None
t_events: None
y_events: None
nfev: 102638
njev: 0
nlu: 0
Figure 8. Again, the electron follows a trajectory solely orbiting $N_2$. We see that the bowl-like curve is roughly halfway between when $\theta = 0$ and $\theta = 90$. This is expected, since the initial vertical velocity component is in between those of the two angles.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 135$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Cycle 2 Analysis¶
From cycle 2, we notice the following:
- Partial Ellipsoid: Across all three trials, we see that a partial ellipsoid is traced by the electron trajectories. This is due to the strong Coluomb force of $N_2$; since $N_2 = 5N_1$, the force of $N_2$ on the electron is significantly more than $N_1$, leading the electron to only orbit $N_2$. However, this coefficient of $5$ is not large enough such that the electron's orbit creates a whole ellipsoid around $N_2$, indicating to effect of $N_1$ on the electron; instead, we see that the electron traces bowl-like shapes.
- Curve of Partial Ellipsoid: We see that as $\theta$ nears 90 degrees, the curve of the partial ellipsoid increases, such that the electron is approaches $N_1$ more closely. This can be explained due to the initial velocity components of teh electron. At $\theta = 90$, the electron's velocity is purely in the y-direction. As a result, it takes a longer time for the $N_2$ Coulomb force to decelerate the electron and reverse its direction back towards $N_2$, allowing the electron to travel closer to $N_1$ each cycle.
- Electron Path Density: We see that the curves traced by the electron trajectory are much denser than in cycle 1. This is due to its orbital path; due to the large $N_2$, the electron is confinedd to only orbit $N_2$, meaning that the distance travelled per period is less. So, for the same time span, the electron travels more periods, increasing trajectory density.
Cycle 3: $N_2 = \frac{1}{5}N_1$, Altering $\theta$¶
For our third cycle, we let $N_2 = \frac{1}{5}N_1$, and we set $\theta = 70, 90, 135$.
Trial 1: $\theta = 70^o$¶
We investigate electron trajectories when $N_2 = \frac{1}{5}N_1$ have equal charges and $\theta = 70^o$.
# Parameters to vary
n2 = 0.2 * 1.6e-19 # Nucleus 2 charge
angle = 70 # Launch angle in degrees
T = 1.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.3, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.906e-18 ... 1.500e-14 1.500e-14]
y: [[ 5.290e-11 5.426e-11 ... 7.681e-11 7.772e-11]
[ 0.000e+00 3.949e-12 ... -8.773e-11 -8.262e-11]
[ 7.456e+05 6.838e+05 ... 2.377e+05 1.881e+05]
[ 2.049e+06 2.094e+06 ... 1.178e+06 1.223e+06]]
sol: None
t_events: None
y_events: None
nfev: 90656
njev: 0
nlu: 0
Figure 9. Again, the electron orbits both $N_1$ and $N_2$ in a ring-like shape. We see that the ring is more spherical due to the horizontal velocity component; since there is less y component in the electron's speed, it does not travel past $N_1$ and $N_2$ as much as instead immediately reverses direction.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 70$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectoryis made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 2: $\theta = 90^o$¶
We investigate electron trajectories when $N_2 = \frac{1}{5}N_1$ have equal charges and $\theta = 90^o$.
# Parameters to vary
n2 = 0.2 * 1.6e-19 # Nucleus 2 charge
angle = 90 # Launch angle in degrees
T = 1.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.25, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 3.191e-23 ... 1.500e-14 1.500e-14]
y: [[ 5.290e-11 5.290e-11 ... 1.133e-10 1.136e-10]
[ 0.000e+00 6.957e-17 ... -4.142e-11 -4.011e-11]
[ 1.335e-10 -1.000e+00 ... 2.407e+05 2.271e+05]
[ 2.180e+06 2.180e+06 ... 1.147e+06 1.155e+06]]
sol: None
t_events: None
y_events: None
nfev: 49634
njev: 0
nlu: 0
Figure 10. The electron follows a trajectory that solely orbits both $N_1$ and $N_2$. Unlike Cycles 1 and 2, the path that the electron traces is ring-like, where it only travels outside a 2-dimensional ellipse.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step. A graph of three subplots for the electron's trajectory is made. The three time plots represent different time spans to highlight both the path the electron takes and the overall shape it traces.
Trial 3: $\theta = 135^o$¶
Finally, we investigate electron trajectories when $N_2 = \frac{1}{2}N_1$ have equal charges and $\theta = 135^0$.
Figure 10. Contrasting trials 1 and 2, we notice that the electron does not trace a ring-like pattern anymore. This, again, is due to the lower vertical velocity; since there is less of a y component in the electron's velocity, the electron does not have enough speed to pass $N_1$ and $N_2$ before being "pulled into" the nuclei.
# Parameters to vary
n2 = 5*0.2 * 1.6e-19 # Nucleus 2 charge
angle = 135 # Launch angle in degrees
T = 1.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.2, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.670e-18 ... 1.500e-14 1.500e-14]
y: [[ 5.290e-11 5.025e-11 ... -2.319e-11 -2.318e-11]
[ 0.000e+00 2.575e-12 ... -5.716e-11 -5.748e-11]
[-1.541e+06 -1.629e+06 ... 4.340e+04 7.763e+04]
[ 1.541e+06 1.543e+06 ... -4.240e+06 -4.243e+06]]
sol: None
t_events: None
y_events: None
nfev: 154856
njev: 0
nlu: 0
Figure 11. The electron traces an elliptical-like journey, similar to Cycle 1. The lack of enough vertical velocity deters the electron to move past each proton before reversing direction in a ring shape.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 135$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.
Code Block Summary: This block creates three subplots for the electron's trajectory. The three time plots represent different tiem spans to highlight both the path the electron takes and the overall shape it traces.
Cycle 3 Analysis¶
From cycle 3, we notice the following:
- Ring Shape: When $\theta$ is close to 90, we see that the electron traces a ring-like shape. This is due to the large y-velocity component; there is enough vertical speed for the electron to travel past the protons before reversing direction. This behavior is only seen when $N_2$ has a small nucleic charge, reducing its Coulomb force on the electron. As $\theta$ moves away from 90, the ring thickens due to the decrease in y-velocity.
- Diverging from a Ring: Eventually, when $\theta$ is no longer dear 90 degrees, we see that the electron no longer traces a ring-like shape, and instead establishes a pattern similar to what is seen in Cycle 2.
Cycle 4: $N_2 = 100N_1$, Altering $\theta$¶
For our final cycle, we let $N_2 = 100N_1$, and we set $\theta = 90, 70, 45$.
Trial 1: $\theta = 90^o$¶
We investigate electron trajectories when $N_2 = 500N_1$ have equal charges and $\theta = 90^o$.
def get_trajectory(n2, angle, T):
# Initial Velocity
v0 = 2.18e6 # initial velocity (m/s)
theta = math.radians(angle) #convert angle to radians, rad
vx0 = math.cos(theta) * v0 # initial x velocity (m/s)
vy0 = math.sin(theta) * v0 # initial y velocity (m/s)
state0 = (r0, 0, vx0, vy0) # (x0, y0, vx0, vy0) (m,m,m/s,m/s)
t_span = (0, T) # span for simulation to run
# Nuclei coordinates
# We define the coordinate (0,0) to be the halfway point between N1 and N2
distance = 1.2 * 10**(-10) #vertical distance between two nuclei, m
y1 = distance / 2 # N1 y coordinate, m
x1 = 0 # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0 # N2 y coordinate, m
# Get your solution from solve_ivp
sol1 = solve_ivp(diff_eqns, t_span, state0, rtol=1e-9, atol=1e-9)
print(sol1)
return sol1
# Define your diff_eqns function
def diff_eqns(t, state):
x, y, vx, vy = state #state variables in timestep, m, m, m/s, m/s
r1x = x - x1 #x distance between electron & N1, m
r1y = y - y1 #y distance between electron & N1, m
r1 = np.sqrt(r1x**2+r1y**2) #distance between N1 and electron
r2x = x - x2 #x distance between electron & N2, m
r2y = y - y2 #y distance between electron & N2, m
r2 = np.sqrt(r2x**2+r2y**2) #distance between N2 and electron
# Calculate the magnitude of the force
# Calculate force & acceleration components for N1
fx1 = k * e * n1 * r1x / r1**3
fy1 = k * e * n1 * r1y / r1**3
# Calculate force & acceleration components for N1
fx2= k * e * n2 * r2x / r2**3
fy2 = k * e * n2 * r2y / r2**3
fx = fx1 + fx2
fy = fy1 + fy2
# Calculate acceleration
accx = fx/me
accy = fy/me
# Return differentials
return vx, vy, accx, accy
def plot_trajectory1(sol1, top, T, c):
# Plot y vs x for theta = 5
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 9)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=top)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_xlim(x_min, x_max)
a.set_ylim(y_min, c * y_max)
plt.show()
# Parameters to vary
n2 = 100 * 1.6e-19 # Nucleus 2 charge
angle = 90 # Launch angle in degrees
T = 2.5e-16 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory1(sol1, 1.2, T, 10)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 3.791e-25 ... 2.500e-16 2.500e-16]
y: [[ 5.290e-11 5.290e-11 ... 4.150e-11 4.172e-11]
[ 0.000e+00 8.265e-19 ... -4.514e-11 -4.481e-11]
[ 1.335e-10 -1.000e+00 ... 1.269e+07 1.248e+07]
[ 2.180e+06 2.180e+06 ... 1.888e+07 1.880e+07]]
sol: None
t_events: None
y_events: None
nfev: 36386
njev: 0
nlu: 0
Figure 12. The electron follows a trajectory that solely orbits $N_2$. Unlike Cycle 2, since the force on $N_2$ is magnitudes larger tha $N_1$, the effect on $N_1$ is negligible, making teh electron follow an elliptical path around the electron. Note that due to the initial conditions of the electron, (such as velocity), the electron's paths still drift, forming a shape resemblimg a thick rubber band.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 90$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.
Trial 2: $\theta = 45^o$¶
We investigate electron trajectories when $N_2 = 100N_1$ have equal charges and $\theta = 45^o$.
# Parameters to vary
n2 = 100 * 1.6e-19 # Nucleus 2 charge
angle = 45 # Launch angle in degrees
T = 2.5e-16 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.1, T, 80)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.448e-19 ... 2.500e-16 2.500e-16]
y: [[ 5.290e-11 5.310e-11 ... 4.492e-11 4.512e-11]
[ 0.000e+00 1.925e-13 ... -3.531e-11 -3.499e-11]
[ 1.541e+06 1.161e+06 ... 9.704e+06 9.536e+06]
[ 1.541e+06 1.119e+06 ... 1.621e+07 1.611e+07]]
sol: None
t_events: None
y_events: None
nfev: 44780
njev: 0
nlu: 0
Figure 13. Again, the electron orbits both $N_2$ in an ellipse. However, we notice that the elliptical "hole" in the previous trial is not present anymore. This indicates that the drift velocity of every period is lower.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 45$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.
Figure 13. Again, the electron orbits both $N_2$ in an ellipse. Drift is further reduced.
Trial 3: $\theta = 20$¶
We investigate electron trajectories when $N_2 = 100N_1$ have equal charges and $\theta = 20^o$.
# Parameters to vary
n2 = 100 * 1.6e-19 # Nucleus 2 charge
angle = 20 # Launch angle in degrees
T = 2.5e-16 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.2, T, 80)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.321e-19 ... 2.500e-16 2.500e-16]
y: [[ 5.290e-11 5.315e-11 ... 4.294e-11 4.341e-11]
[ 0.000e+00 7.298e-14 ... -3.545e-11 -3.477e-11]
[ 2.049e+06 1.701e+06 ... 1.130e+07 1.093e+07]
[ 7.456e+05 3.596e+05 ... 1.630e+07 1.608e+07]]
sol: None
t_events: None
y_events: None
nfev: 48146
njev: 0
nlu: 0
Figure 14. Again, the electron orbits both $N_2$ in an ellipse. However, the electron experiences less drift to the the the angle.
Code Block Summary: This block simulates the electron's trajectory when $\theta = 20$. A function is defined to determine the Coulomb forces and acceleration on the electron at any given time step, which are then returned for Solve_IVP to use. Sol1 outputs the electron's positions and velocities at each time step.
Cycle 4 Analysis¶
From cycle 4, we notice the following:
- Flat Ellipsoid: The electron only orbits $N_2$ in an ellptic shape due to its large force compared to $N_1$. Crucially, teh electron's trajectory is different from that of cycle 1; while electrons form a full, 3-dimensional ellipsoid in cycle 1, the electron creates a flat, rubber-band-like shape in cycle 4. This is due to the drift the electron experiences across time
- Symmetric electron path: Unlike in the other cycles, cycle 4 results show how the electron's trajectory every period is unchaotic; its 3-dimensional shape is due not to the variations within one period, but due to the electron's drift.
- Drift: As we alter the angle, the electron's trajectory shift changes, due to the respective y and x components of initial velocity. Specifically, as $\theta$ moves away from 90 degrees, the drift decreases.
Part II: Discrete Phase-Space Analysis to Detect Stable Orbits¶
In Part II, we add additional event detection to quantitatively detect stable orbits across a phase space of $N_2$ and $\theta$. In this phase space, we define three categories of orbits:
- Stable Periodic: These orbits have relative standard deviations of under 10%, and do not collide with either nucleus.
- Unstable/Quasi-Periodic: These orbits have relative deviations of over 10%, but also do not collide with either nucleus.
- Collision: These orbits collides with at least 1 of the 2 nuclei. As a result, periodicity is not checked.
Programming for stability detection¶
To determine stability, we must check for (1) periodicity and (2) electron-proton collision. To check for periodicity, we create a function that calculates the negative distance between the electron and its original position for each time step. We then use SciPy’s “find_peaks” function to determine the local minima of each of the distances; since the function finds the local maxima, we insert the negative of each distance value into an array into "find_peaks" to determine the local minima. The times of each of these distances is then indexed out of the solution array outputted by Solve_IVP, and the periods are calculated by subtracting the times from each other. The relative standard deviation is then calculated and compared to the 5% threshold.
For electron-proton collision, we create functions to calculate the distance between the electron and both protons at all time-steps. Each distance is then compared with the distance threshold of 8.5e-16m to determine if an electron-proton collision occurs.
Parameters¶
We set each simulation to run for $2.5 e -15$ s; this was the longest time the code could run for the phase space to output results in less than 1.5 hours. We run 9000 simulations in our phase space domains of $0 \leq \theta \leq 90^o$ and $\frac{1}{100}N_1 \leq N_2 \leq 100N_1$ by creating a 90-element linear space for $\theta$ and 100-element linear space for $N_2$. As a reminder, $\theta = 0$ refers to the initial velocity vector parallel with the positive x axis, and $\theta = 90$ refers to the initial velocity vector parallel with the line defined by $N_1$ and $N_2$.
Detecting Stable Orbits¶
The code below checks for stable orbits across the chosen phase space.
# SIMULATION CODE
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.signal import find_peaks
import math
import time
# Fixed Constants
k = 8.99e9 # Coulomb constant, N·m²/C²
e = -1.6e-19 # Electron charge magnitude, C
n1 = 1.6e-19 # Nucleus 1 charge
me = 9.109e-31 # Electron mass, kg
r0 = 5.29e-11 # Bohr radius, m
# Orbital time
T = 2.5e-15 # time for simulation to run, s
# Nuclei coordinates
# We define the coordinate (0,0) to be the halfway point between N1 and N2
distance = 1.2e-10 # vertical distance between two nuclei, m
y1 = distance / 2 # N1 y coordinate, m
x1 = 0 # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0 # N2 y coordinate, m
# Initial Velocity
v0 = 2.18e6 # initial velocity (m/s)
# Define your diff_eqns function
def diff_eqns(t, state):
x, y, vx, vy = state # state variables in timestep, m, m, m/s, m/s
r1x = x - x1 # x distance between electron & N1, m
r1y = y - y1 # y distance between electron & N1, m
r1 = np.sqrt(r1x**2 + r1y**2) # distance between N1 and electron
r2x = x - x2 # x distance between electron & N2, m
r2y = y - y2 # y distance between electron & N2, m
r2 = np.sqrt(r2x**2 + r2y**2) # distance between N2 and electron
# Avoid singularities
r1 = max(r1, 1e-20)
r2 = max(r2, 1e-20)
# Calculate force & acceleration components for N1
fx1 = k * e * n1 * r1x / r1**3
fy1 = k * e * n1 * r1y / r1**3
# Calculate force & acceleration components for N2
fx2 = k * e * n2 * r2x / r2**3
fy2 = k * e * n2 * r2y / r2**3
fx = fx1 + fx2
fy = fy1 + fy2
# Calculate acceleration
accx = fx / me
accy = fy / me
# Return differentials
return vx, vy, accx, accy
Code Block Summary: This above code sets the initial constants used in the rest of the simulation. It also defines the differential equations needed to be inputted into Solve_IVP, including vertical velocity, horizontal velocity, vertical acceleration, and horizontal acceleration. These were calculated using the equation for Coulomb's force.
# NEW DETECTION FUNCTIONS
def check_collision(sol, collision_threshold=8.5e-16):
# Calculate distances to each nucleus at all time steps
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
# Check collision with nucleus 1
collision_n1_indices = np.where(r1 < collision_threshold)[0]
# Check collision with nucleus 2
collision_n2_indices = np.where(r2 < collision_threshold)[0]
collided = False
if len(collision_n1_indices) > 0:
collided = True
elif len(collision_n2_indices) > 0:
collided = True
return collided
Code Block Summary: The above code defines a function that checks for electron-proton collisions. For each time step, the function calculates the distance between the electron and its original position. Using this, it compares each difference to the collision threshold to output a boolean of whether the electron experiences a collision.
def check_periodicity(sol, rel_std_threshold=0.10):
if len(sol.t) < 10:
return False, None
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
try:
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
except:
return False, None
if len(peaks) < 2:
return False, None
# Calculate periods
periods = np.diff(sol.t[peaks])
if len(periods) < 2:
return False, None
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
# Check if variation is within threshold
is_periodic = period_std_rel < rel_std_threshold
return is_periodic
Code Block Summary: The above code defines a function that checks for periodicity. For each time step, the function calculates the distacne between the electron and its original position. Using this, the local minima of the distances are calculated, and the times for each of these "peaks" are found. The difference between each time is then calculated to be the "period" of each cycle, and the relative standard deviation is calculated and compared to the threshold relative standard deviation.
def analyze_single_orbit(angle, n2_value):
try:
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Set n2 as global for diff_eqns to use
global n2
n2 = n2_value
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check for collision first
collided = check_collision(sol)
if collided:
return 2 # Collision
# Check periodicity
is_periodic = check_periodicity(sol)
if is_periodic:
return 0 # Stable periodic
else:
return 1 # Unstable
except Exception as e:
return 3 # Error
Code Block Summary: The above code integrates the previous two functions to analyze a single orbit. The code is the same as the code used in Part I, except it incorporates the collision and periodicity checks insto a single function.
# PHASE SPACE SCAN
def scan_phase_space_2d(n_theta=30, n_n2=40):
print("="*70)
print("2D PHASE SPACE SCAN: THETA vs N2")
print("="*70)
# Phase space ranges
theta_range = np.linspace(0, 90, n_theta) # 0 to 90 degrees
n2_n1_ratios = np.logspace(-2, 2, n_n2) # 0.01 to 100 (N1/100 to 100*N1)
n2_range = n1 * n2_n1_ratios
print(f"Theta axis: {n_theta} points from 0° to 90°")
print(f"N2 axis: {n_n2} points from N1/100 to 100*N1")
print(f" N2/N1 range: {n2_n1_ratios[0]:.3f} to {n2_n1_ratios[-1]:.1f}")
print(f" N2 range: {n2_range[0]:.3e} to {n2_range[-1]:.3e} C")
print(f"Total simulations: {n_theta * n_n2}")
print(f"Simulation time per orbit: {T:.2e} s")
print("="*70 + "\n")
# Initialize results grid
results_grid = np.zeros((n_n2, n_theta))
total_sims = n_theta * n_n2
sim_count = 0
start_time = time.time()
# Run simulations
for i, n2_val in enumerate(n2_range):
print(f"\nN2/N1 = {n2_val/n1:.3f} ({i+1}/{n_n2})")
for j, angle_val in enumerate(theta_range):
sim_count += 1
# Progress update every 5 simulations
if sim_count % 5 == 0 or sim_count == total_sims:
elapsed = time.time() - start_time
rate = sim_count / elapsed if elapsed > 0 else 0
eta = (total_sims - sim_count) / rate if rate > 0 else 0
print(f" Progress: {sim_count}/{total_sims} ({100*sim_count/total_sims:.1f}%) "
f"| Rate: {rate:.1f} sim/s | ETA: {eta:.0f}s", end='\r')
# Analyze this configuration
status = analyze_single_orbit(angle_val, n2_val)
results_grid[i, j] = status
elapsed = time.time() - start_time
print(f"\n\nScan completed in {elapsed:.1f}s ({elapsed/60:.1f} min)")
print(f"Average: {elapsed/total_sims:.2f}s per simulation\n")
# Print statistics
n_stable = np.sum(results_grid == 0)
n_unstable = np.sum(results_grid == 1)
n_collision = np.sum(results_grid == 2)
n_error = np.sum(results_grid == 3)
print("Results Summary:")
print(f" Stable periodic: {n_stable:4d} ({100*n_stable/total_sims:.1f}%)")
print(f" Unstable/quasi: {n_unstable:4d} ({100*n_unstable/total_sims:.1f}%)")
print(f" Collision: {n_collision:4d} ({100*n_collision/total_sims:.1f}%)")
print(f" Error: {n_error:4d} ({100*n_error/total_sims:.1f}%)")
print("="*70 + "\n")
return results_grid, theta_range, n2_range
Code Block Summary: The above code incoroporates the previous code block to scan the entire phase space. It includes a progress check, where the progress rate is updated every 5 simulations. At the end, the function returns an array of all the results for each of the 9000 simulations.
# Plotting results
def plot_phase_space_2d(results_grid, theta_range, n2_range):
from matplotlib.colors import ListedColormap
fig = plt.figure(figsize=(14, 10))
# Create meshgrid
theta_grid, n2_grid = np.meshgrid(theta_range, n2_range)
n2_n1_grid = n2_grid / n1
# Define colormap
colors = ["#6420a6", "#b5318c", "#f48d47", "#f6e724"]
cmap = ListedColormap(colors)
labels = ['Stable Periodic', 'Unstable/Quasi-periodic', 'Collision', 'Error']
# Main 2D phase space plot
ax = fig.add_subplot(111)
im = ax.pcolormesh(theta_grid, n2_n1_grid, results_grid,
cmap=cmap, vmin=0, vmax=3, shading='auto',
edgecolors='face', linewidth=0)
ax.set_xlabel('Initial Velocity Angle θ (degrees)', fontsize=14, fontweight='bold')
ax.set_ylabel('Charge Ratio N2/N1', fontsize=14, fontweight='bold')
ax.set_title('Phase Space: Orbital Stability Map\nθ ∈ [0°, 90°], N2 ∈ [N1/100, 100×N1] \n V0 = 2.18e6 m/s (Fixed) ',
fontsize=16, fontweight='bold', pad=20)
# Use log scale for N2/N1
ax.set_yscale('log')
# Set y-axis limits and ticks
ax.set_ylim([0.01, 100])
ax.set_yticks([0.01, 0.1, 1, 10, 100])
ax.set_yticklabels(['0.01', '0.1', '1', '10', '100'])
# Set x-axis limits
ax.set_xlim([0, 90])
# Add grid
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
# Add reference line at N2 = N1
ax.axhline(y=1, color='white', linestyle='--', linewidth=2.5,
alpha=0.8, label='N2 = N1 (equal charges)')
# Add reference lines for key angles
ax.axvline(x=0, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
ax.axvline(x=45, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
ax.axvline(x=90, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
# Create legend for stability regions
from matplotlib.patches import Patch
# Add colorbar
cbar = plt.colorbar(im, ax=ax, ticks=[0.375, 1.125, 1.875, 2.625],
pad=0.02, aspect=30)
cbar.ax.set_yticklabels(labels, fontsize=10)
cbar.ax.tick_params(size=0)
# Add text annotations for interesting regions
ax.text(5, 0.015, 'Low angle\nLow charge', fontsize=9,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
ax.text(85, 80, 'High angle\nHigh charge', fontsize=9, ha='right',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
plt.tight_layout()
plt.savefig('phase_space_2d_theta_vs_n2.png', dpi=300, bbox_inches='tight')
print("Plot saved as 'phase_space_2d_theta_vs_n2.png'")
plt.show()
return fig
Code Block Summary: Given the grid of results calculated from the previous block, this function plots all the data into one graph and returns the figure.
Examples of Periodicity Check¶
As an example, below is a histogram of periods generated by the periodicity check function, when $\theta = 35$ and $N_2 = 0.8N_1$. This is a stable orbit.
# Set up initial conditions
angle = 35
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 5e-15 # time for simulation to run, s
# Set n2 as global for diff_eqns to use
n2 = 0.8 * 1.6e-19
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2e} s\n' \
f'Rel. Std Dev: {period_std_rel:.2%}\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 15 Mean period: 3.08e-16 s Standard deviation: 3.61e-19 s Relative std dev: 0.12% Minimum period: 3.07e-16 s Maximum period: 3.08e-16 s Range: 1.42e-18 s Is the orbit periodic? Yes ==================================================
Figure 15. A roughly-Gaussian distribution is seen for the orbital periods of a stable orbit. standard deviation is 0.12%.
On the contrary, below is a histogram of periods generated by the periodicity check function, when $\theta = 40$ and $N_2 = 50N_1$. This is a quasi-periodic, unstable orbit.
# Set up initial conditions
angle = 40
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 5e-15 # time for simulation to run, s
# Set n2 as global for diff_eqns to use
n2 = 60 * 1.6e-19
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2e} s\n' \
f'Rel. Std Dev: {period_std_rel:.2%}\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 18 Mean period: 2.76e-16 s Standard deviation: 6.13e-17 s Relative std dev: 22.21% Minimum period: 1.72e-16 s Maximum period: 3.55e-16 s Range: 1.83e-16 s Is the orbit periodic? No ==================================================
Figure 16. A random distribution of orbital periods can be seen for an unstable orbit. Relative standard deviation is 22.21%.
We see that the periods for the latter is much more randomnly distributed, indicating a higher relative standard uncertainty and overall unstable orbit.
Part I Phase Space Simulation: Discrete Categories¶
# MAIN CODE
# Run phase space scan
print("\n" + "="*70)
input("Press Enter to start 2D phase space scan...")
results_grid, theta_range, n2_range = scan_phase_space_2d(n_theta=10, n_n2=10)
# Plot results
plot_phase_space_2d(results_grid, theta_range, n2_range)
# Save results
np.savez('phase_space_2d_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'phase_space_2d_results.npz'")
====================================================================== ====================================================================== 2D PHASE SPACE SCAN: THETA vs N2 ====================================================================== Theta axis: 10 points from 0° to 90° N2 axis: 10 points from N1/100 to 100*N1 N2/N1 range: 0.010 to 100.0 N2 range: 1.600e-21 to 1.600e-17 C Total simulations: 100 Simulation time per orbit: 5.00e-15 s ====================================================================== N2/N1 = 0.010 (1/10) Progress: 10/100 (10.0%) | Rate: 13.2 sim/s | ETA: 7s N2/N1 = 0.028 (2/10) Progress: 20/100 (20.0%) | Rate: 11.5 sim/s | ETA: 7s N2/N1 = 0.077 (3/10) Progress: 30/100 (30.0%) | Rate: 9.7 sim/s | ETA: 7ss N2/N1 = 0.215 (4/10) Progress: 40/100 (40.0%) | Rate: 7.8 sim/s | ETA: 8s N2/N1 = 0.599 (5/10) Progress: 50/100 (50.0%) | Rate: 5.3 sim/s | ETA: 10s N2/N1 = 1.668 (6/10) Progress: 60/100 (60.0%) | Rate: 3.6 sim/s | ETA: 11s N2/N1 = 4.642 (7/10) Progress: 70/100 (70.0%) | Rate: 2.6 sim/s | ETA: 12s N2/N1 = 12.915 (8/10) Progress: 80/100 (80.0%) | Rate: 1.6 sim/s | ETA: 12s N2/N1 = 35.938 (9/10) Progress: 90/100 (90.0%) | Rate: 1.0 sim/s | ETA: 10s N2/N1 = 100.000 (10/10) Progress: 100/100 (100.0%) | Rate: 0.7 sim/s | ETA: 0s Scan completed in 143.4s (2.4 min) Average: 1.43s per simulation Results Summary: Stable periodic: 14 (14.0%) Unstable/quasi: 55 (55.0%) Collision: 31 (31.0%) Error: 0 (0.0%) ====================================================================== Plot saved as 'phase_space_2d_theta_vs_n2.png'
Results saved to 'phase_space_2d_results.npz'
Code Block Summary. The above code is the main code function that executes the phase space scan, incorporating all previous functions. The graph si saved as an npz file after compiling.
Image of Phase-Space Simulation¶

Figure 17. Image of the phase-space simulation. As the $N_2/N_1$ ratio increases, the number of periodic orbits decreases, and is replaced by unstable orbits and collisions.
From the phase space simulation, we see that as $\frac{N_2}{N_1}$ increases, the number of periodic orbits decreases and is replaced by unstable orbits, and as it further increases, those unstable orbits transition into collisions. However, we also see the effect of initial velocity angle on stabilities: specifically, there seems to be a curving band of stable orbits above the typical region, as well as an area on the right of the phase space of stable, unstable, and collision orbits. We pick 1 $N_2, \theta$ combination for each of these regions and observe their electron trajectories.
We first redefine analyze_single_orbit() to take period as a parameter. Note that the time spans we use to plot each region will be different; this is because we want to show a representative amount of the electron's trajectory for each plot, which we cannot do at a constant time span. However, the time span used to determine collisions and periodicity is constant.
# Redefine analyze orbit with period as a parameter
def analyze_single_orbit(angle, n2_value,T):
try:
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Set n2 as global for diff_eqns to use
global n2
n2 = n2_value
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check for collision first
collided = check_collision(sol)
if collided:
return 2, sol # Collision
# Check periodicity
is_periodic = check_periodicity(sol)
if is_periodic:
return 0, sol # Stable periodic
else:
return 1, sol # Unstable
except Exception as e:
return 3, sol # Error
Region 1: Lower Third¶

We first analyze a trajectory in the lower third region of the phase space graph. We set $N_2 = 0.05N_1$ and $\theta = 20$.
# Region 1: Typical Region
T = 2.5e-15 # time for simulation to run, s
T1 = 2.5e-14
n2 = 0.05 * 1.6e-19
angle = 20
result, sol = analyze_single_orbit(angle, n2,T)
if result == 0:
print("Status: Stable periodic orbit")
elif result == 1:
print("Status: Unstable orbit")
elif result == 2:
print("Status: Collision with nucleus")
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1.4, T1, 1)
Status: Stable periodic orbit
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 2.416e-18 ... 2.500e-14 2.500e-14]
y: [[ 5.290e-11 5.777e-11 ... -4.156e-11 -4.499e-11]
[ 0.000e+00 1.881e-12 ... 2.474e-10 2.445e-10]
[ 2.049e+06 1.982e+06 ... -7.161e+05 -7.084e+05]
[ 7.456e+05 8.102e+05 ... -5.667e+05 -5.998e+05]]
sol: None
t_events: None
y_events: None
nfev: 51710
njev: 0
nlu: 0
Figure 18. Electron orbital trajectory when $N_2 = 0.05N_1$. Electron solely orbits $N_1$. and does so along elliptical paths that create a partial-ellipsoid like shape.
Region 2: Curving Band¶

Next, we analyze the region with curving band. We set $N_2 = 0.8N_1$ and $\theta = 35$.
# Region 2: Curving Band
T = 2e-15 # time for simulation to run, s
n2 = 0.8 * 1.6e-19
angle = 35
result, sol = analyze_single_orbit(angle, n2,T)
if result == 0:
print("Stable periodic orbit")
elif result == 1:
print("Unstable orbit")
elif result == 2:
print("Collision with nucleus")
T1 = 1e-14
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1.25, T1, 1)
Stable periodic orbit
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.896e-18 ... 1.000e-14 1.000e-14]
y: [[ 5.290e-11 5.620e-11 ... -1.965e-11 -2.033e-11]
[ 0.000e+00 2.382e-12 ... -1.990e-11 -1.952e-11]
[ 1.786e+06 1.697e+06 ... -2.561e+06 -2.547e+06]
[ 1.250e+06 1.262e+06 ... 1.420e+06 1.406e+06]]
sol: None
t_events: None
y_events: None
nfev: 77798
njev: 0
nlu: 0
Figure 19. Electron orbital trajectory when $N_2 = 0.8N_1$ and $\theta = 35$. Electron orbits both $N_1$ and $N_2$ in a figure-eight pattern, tracing our an ellipsoid.
Region 3: Chaotic Region¶

Finally, we analyze the region of the phase space diagram with stable, unstable, and collision orbits.
# Region 3: Chaotic Region
T = 2.5e-15 # time for simulation to run, s
n2 = 1.54e-19 #0.9625 * 1.6
angle = 86
result, sol = analyze_single_orbit(angle, n2,T)
if result == 0:
print("Stable periodic orbit")
elif result == 1:
print("Unstable orbit")
elif result == 2:
print("Collision with nucleus")
T1 = 2.5e-15
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1.1, T1, 1)
Stable periodic orbit
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.085e-18 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.304e-11 ... -4.548e-11 -4.525e-11]
[ 0.000e+00 2.362e-12 ... 3.847e-11 3.897e-11]
[ 1.521e+05 9.673e+04 ... 1.105e+06 1.126e+06]
[ 2.175e+06 2.177e+06 ... 2.383e+06 2.388e+06]]
sol: None
t_events: None
y_events: None
nfev: 57044
njev: 0
nlu: 0
Figure 20. Electron trajectory when $N_2 = 0.9625 N_1$ and $\theta = 86$. Electron orbits both $N_1$ and $N_2$ in a loop, tracing out a whole three-dimensional ellipsoid shape.
While this orbit in the chaotic region seems very stable, we observe what occurs after alterative $N_2$ by $0.005e-19$ C.
# Region 3: Chaotic Region
T = 2.5e-15 # time for simulation to run, s
n2 = 1.545e-19 #0.9625 * 1.6
angle = 86
result, sol = analyze_single_orbit(angle, n2,T)
if result == 0:
print("Stable periodic orbit")
elif result == 1:
print("Unstable orbit")
elif result == 2:
print("Collision with nucleus")
T1 = 2.5e-15
sol1 = get_trajectory(n2, angle, T1)
plot_trajectory(sol1, 1, T1, 1)
Collision with nucleus
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.083e-18 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.303e-11 ... -2.977e-12 -2.721e-12]
[ 0.000e+00 2.356e-12 ... 3.404e-11 3.505e-11]
[ 1.521e+05 9.643e+04 ... 1.009e+06 1.020e+06]
[ 2.175e+06 2.177e+06 ... 3.980e+06 4.070e+06]]
sol: None
t_events: None
y_events: None
nfev: 66332
njev: 0
nlu: 0
Figure 21. At $N_2 = 0.9656N_1$, the electron exhibits a collision with a nucleus.
Part III: Continuous Phase-Space Analysis to Quantify Relative Standard Deviation¶
In the final part of our analysis, we extend our discrete phase-space simulation into a quantitative heat map. Instead of showing the the stepwise categories of orbit, we display a continuous heat map of the relative standard deviations of each orbit in our phase space. However, if the orbit is a collision orbit, we override its value on the heat map with a solid red colour to indicate its collision. We first slightly alter our previous code to output a continuous heat map, the execute the main function.
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.signal import find_peaks
import math
import time
# Fixed Constants
k = 8.99e9 # Coulomb constant, N·m²/C²
e = -1.6e-19 # Electron charge magnitude, C
n1 = 1.6e-19 # Nucleus 1 charge
me = 9.109e-31 # Electron mass, kg
r0 = 5.29e-11 # Bohr radius, m
# Orbital time
T = 2.5e-15 # time for simulation to run, s
# Nuclei coordinates
distance = 1.2e-10 # vertical distance between two nuclei, m
y1 = distance / 2 # N1 y coordinate, m
x1 = 0 # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0 # N2 y coordinate, m
# Initial Velocity
v0 = 2.18e6 # initial velocity (m/s)
# Define your diff_eqns function
def diff_eqns(t, state):
x, y, vx, vy = state
r1x = x - x1
r1y = y - y1
r1 = np.sqrt(r1x**2 + r1y**2)
r2x = x - x2
r2y = y - y2
r2 = np.sqrt(r2x**2 + r2y**2)
# Avoid singularities
r1 = max(r1, 1e-20)
r2 = max(r2, 1e-20)
# Calculate force & acceleration components for N1
fx1 = k * e * n1 * r1x / r1**3
fy1 = k * e * n1 * r1y / r1**3
# Calculate force & acceleration components for N2
fx2 = k * e * n2 * r2x / r2**3
fy2 = k * e * n2 * r2y / r2**3
fx = fx1 + fx2
fy = fy1 + fy2
# Calculate acceleration
accx = fx / me
accy = fy / me
# Return differentials
return vx, vy, accx, accy
# REVISED DETECTION FUNCTIONS
def check_collision(sol, collision_threshold=8.5e-16):
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
collision_n1_indices = np.where(r1 < collision_threshold)[0]
collision_n2_indices = np.where(r2 < collision_threshold)[0]
if len(collision_n1_indices) > 0 or len(collision_n2_indices) > 0:
return True
return False
def calculate_period_std(sol):
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
try:
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
except:
return None
if len(peaks) < 2:
return None
# Calculate periods
periods = np.diff(sol.t[peaks])
if len(periods) < 2:
return None
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
if period_mean == 0:
return None
period_std_rel = period_std / period_mean
return period_std_rel
def analyze_single_orbit_quantitative(angle, n2_value):
try:
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Set n2 as global for diff_eqns to use
global n2
n2 = n2_value
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check for collision first
collided = check_collision(sol)
if collided:
return -1 # Collision marker
# Calculate period variability
rel_std = calculate_period_std(sol)
return rel_std
except Exception as e:
return None # Error
# PHASE SPACE SCAN
def scan_phase_space_2d_quantitative(n_theta=30, n_n2=40):
print("="*70)
print("2D QUANTITATIVE PHASE SPACE SCAN: Period Variability")
print("="*70)
# Phase space ranges
theta_range = np.linspace(0, 90, n_theta)
n2_n1_ratios = np.logspace(-2, 2, n_n2)
n2_range = n1 * n2_n1_ratios
print(f"Theta axis: {n_theta} points from 0° to 90°")
print(f"N2 axis: {n_n2} points from N1/100 to 100*N1")
print(f" N2/N1 range: {n2_n1_ratios[0]:.3f} to {n2_n1_ratios[-1]:.1f}")
print(f"Total simulations: {n_theta * n_n2}")
print("="*70 + "\n")
# Initialize results grid (use NaN for missing data)
results_grid = np.full((n_n2, n_theta), np.nan)
total_sims = n_theta * n_n2
sim_count = 0
start_time = time.time()
# Run simulations
for i, n2_val in enumerate(n2_range):
print(f"\nN2/N1 = {n2_val/n1:.3f} ({i+1}/{n_n2})")
for j, angle_val in enumerate(theta_range):
sim_count += 1
if sim_count % 5 == 0 or sim_count == total_sims:
elapsed = time.time() - start_time
rate = sim_count / elapsed if elapsed > 0 else 0
eta = (total_sims - sim_count) / rate if rate > 0 else 0
print(f" Progress: {sim_count}/{total_sims} ({100*sim_count/total_sims:.1f}%) "
f"| Rate: {rate:.1f} sim/s | ETA: {eta:.0f}s", end='\r')
# Analyze this configuration
rel_std = analyze_single_orbit_quantitative(angle_val, n2_val)
results_grid[i, j] = rel_std if rel_std is not None else np.nan
elapsed = time.time() - start_time
print(f"\n\nScan completed in {elapsed:.1f}s ({elapsed/60:.1f} min)")
print(f"Average: {elapsed/total_sims:.2f}s per simulation\n")
# Print statistics
n_collision = np.sum(results_grid == -1)
n_valid = np.sum(~np.isnan(results_grid) & (results_grid != -1))
n_error = np.sum(np.isnan(results_grid))
valid_data = results_grid[(~np.isnan(results_grid)) & (results_grid != -1)]
print("Results Summary:")
print(f" Valid orbits: {n_valid:4d} ({100*n_valid/total_sims:.1f}%)")
print(f" Collisions: {n_collision:4d} ({100*n_collision/total_sims:.1f}%)")
print(f" Errors: {n_error:4d} ({100*n_error/total_sims:.1f}%)")
if len(valid_data) > 0:
print(f"\nPeriod Rel. Std. Dev. Statistics (valid orbits):")
print(f" Min: {np.min(valid_data):.6f}")
print(f" Max: {np.max(valid_data):.6f}")
print(f" Mean: {np.mean(valid_data):.6f}")
print(f" Median: {np.median(valid_data):.6f}")
print("="*70 + "\n")
return results_grid, theta_range, n2_range
# PLOTTING RESULTS
def plot_phase_space_heatmap(results_grid, theta_range, n2_range):
import matplotlib.colors as mcolors
fig = plt.figure(figsize=(14, 10))
# Create meshgrid
theta_grid, n2_grid = np.meshgrid(theta_range, n2_range)
n2_n1_grid = n2_grid / n1
# Separate collision and non-collision data
collision_mask = (results_grid == -1)
data_for_heatmap = results_grid.copy()
data_for_heatmap[collision_mask] = np.nan # Hide collisions from main heatmap
ax = fig.add_subplot(111)
# Plot main heat map (period variability)
im = ax.pcolormesh(theta_grid, n2_n1_grid, data_for_heatmap,
cmap='viridis', shading='auto',
vmin=0, vmax=np.nanpercentile(data_for_heatmap, 95))
# Overlay collision regions in red
collision_data = np.where(collision_mask, 1, np.nan)
ax.pcolormesh(theta_grid, n2_n1_grid, collision_data,
cmap=mcolors.ListedColormap(['#ff0000']),
shading='auto', alpha=0.9)
ax.set_xlabel('Initial Velocity Angle θ (degrees)', fontsize=14, fontweight='bold')
ax.set_ylabel('Charge Ratio N2/N1', fontsize=14, fontweight='bold')
ax.set_title('Phase Space: Orbital Period Variability (Rel. Std. Dev.)\n' +
'θ ∈ [0°, 90°], N2 ∈ [N1/100, 100×N1], V0 = 2.18e6 m/s',
fontsize=16, fontweight='bold', pad=20)
# Use log scale for N2/N1
ax.set_yscale('log')
ax.set_ylim([0.01, 100])
ax.set_yticks([0.01, 0.1, 1, 10, 100])
ax.set_yticklabels(['0.01', '0.1', '1', '10', '100'])
ax.set_xlim([0, 90])
# Add grid
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
# Add reference lines
ax.axhline(y=1, color='white', linestyle='--', linewidth=2.5,
alpha=0.8, label='N2 = N1')
ax.axvline(x=0, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
ax.axvline(x=45, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
ax.axvline(x=90, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
# Add colorbar for period variability
cbar = plt.colorbar(im, ax=ax, pad=0.02, aspect=30)
cbar.set_label('Relative Std. Dev. of Period\n(Lower = More Periodic)',
fontsize=12, fontweight='bold')
# Add legend for collision regions
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor='#ff0000', label='Collision'),
Patch(facecolor='#440154', label='Low Variability (Stable)'),
Patch(facecolor='#fde724', label='High Variability (Chaotic)')
]
ax.legend(handles=legend_elements, loc='upper left', fontsize=10,
framealpha=0.9)
plt.tight_layout()
plt.savefig('phase_space_heatmap_quantitative.png', dpi=300, bbox_inches='tight')
print("Heat map saved as 'phase_space_heatmap_quantitative.png'")
plt.show()
return fig
# MAIN CODE
# Run phase space scan
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")
results_grid, theta_range, n2_range = scan_phase_space_2d_quantitative(n_theta=400, n_n2=500)
# Plot results
plot_phase_space_heatmap(results_grid, theta_range, n2_range)
# Save results
np.savez('phase_space_heatmap_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
====================================================================== ====================================================================== 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability ====================================================================== Theta axis: 400 points from 0° to 90° N2 axis: 500 points from N1/100 to 100*N1 N2/N1 range: 0.010 to 100.0 Total simulations: 200000 ====================================================================== N2/N1 = 0.010 (1/500) Progress: 400/200000 (0.2%) | Rate: 42.3 sim/s | ETA: 4720s N2/N1 = 0.010 (2/500) Progress: 800/200000 (0.4%) | Rate: 42.2 sim/s | ETA: 4717s N2/N1 = 0.010 (3/500) Progress: 1200/200000 (0.6%) | Rate: 42.3 sim/s | ETA: 4697s N2/N1 = 0.011 (4/500) Progress: 1600/200000 (0.8%) | Rate: 42.3 sim/s | ETA: 4693s N2/N1 = 0.011 (5/500) Progress: 2000/200000 (1.0%) | Rate: 42.2 sim/s | ETA: 4689s N2/N1 = 0.011 (6/500) Progress: 2400/200000 (1.2%) | Rate: 42.2 sim/s | ETA: 4684s N2/N1 = 0.011 (7/500) Progress: 2800/200000 (1.4%) | Rate: 42.2 sim/s | ETA: 4676s N2/N1 = 0.011 (8/500) Progress: 3200/200000 (1.6%) | Rate: 42.2 sim/s | ETA: 4665s N2/N1 = 0.012 (9/500) Progress: 3600/200000 (1.8%) | Rate: 42.1 sim/s | ETA: 4663s N2/N1 = 0.012 (10/500) Progress: 4000/200000 (2.0%) | Rate: 42.1 sim/s | ETA: 4656s N2/N1 = 0.012 (11/500) Progress: 4400/200000 (2.2%) | Rate: 42.0 sim/s | ETA: 4652s N2/N1 = 0.012 (12/500) Progress: 4800/200000 (2.4%) | Rate: 42.0 sim/s | ETA: 4648s N2/N1 = 0.012 (13/500) Progress: 5200/200000 (2.6%) | Rate: 42.0 sim/s | ETA: 4641s N2/N1 = 0.013 (14/500) Progress: 5600/200000 (2.8%) | Rate: 41.9 sim/s | ETA: 4635s N2/N1 = 0.013 (15/500) Progress: 6000/200000 (3.0%) | Rate: 41.9 sim/s | ETA: 4627s N2/N1 = 0.013 (16/500) Progress: 6400/200000 (3.2%) | Rate: 41.9 sim/s | ETA: 4617s N2/N1 = 0.013 (17/500) Progress: 6800/200000 (3.4%) | Rate: 41.9 sim/s | ETA: 4611s N2/N1 = 0.014 (18/500) Progress: 7200/200000 (3.6%) | Rate: 41.9 sim/s | ETA: 4603s N2/N1 = 0.014 (19/500) Progress: 7600/200000 (3.8%) | Rate: 41.9 sim/s | ETA: 4596s N2/N1 = 0.014 (20/500) Progress: 8000/200000 (4.0%) | Rate: 41.8 sim/s | ETA: 4589s N2/N1 = 0.014 (21/500) Progress: 8400/200000 (4.2%) | Rate: 41.8 sim/s | ETA: 4583s N2/N1 = 0.015 (22/500) Progress: 8800/200000 (4.4%) | Rate: 41.8 sim/s | ETA: 4575s N2/N1 = 0.015 (23/500) Progress: 9200/200000 (4.6%) | Rate: 41.8 sim/s | ETA: 4568s N2/N1 = 0.015 (24/500) Progress: 9600/200000 (4.8%) | Rate: 41.7 sim/s | ETA: 4561s N2/N1 = 0.016 (25/500) Progress: 10000/200000 (5.0%) | Rate: 41.7 sim/s | ETA: 4555s N2/N1 = 0.016 (26/500) Progress: 10400/200000 (5.2%) | Rate: 41.7 sim/s | ETA: 4548s N2/N1 = 0.016 (27/500) Progress: 10800/200000 (5.4%) | Rate: 41.7 sim/s | ETA: 4541s N2/N1 = 0.016 (28/500) Progress: 11200/200000 (5.6%) | Rate: 41.7 sim/s | ETA: 4533s N2/N1 = 0.017 (29/500) Progress: 11600/200000 (5.8%) | Rate: 41.6 sim/s | ETA: 4526s N2/N1 = 0.017 (30/500) Progress: 12000/200000 (6.0%) | Rate: 41.6 sim/s | ETA: 4518s N2/N1 = 0.017 (31/500) Progress: 12400/200000 (6.2%) | Rate: 41.6 sim/s | ETA: 4511s N2/N1 = 0.018 (32/500) Progress: 12800/200000 (6.4%) | Rate: 41.6 sim/s | ETA: 4504s N2/N1 = 0.018 (33/500) Progress: 13200/200000 (6.6%) | Rate: 41.5 sim/s | ETA: 4497s N2/N1 = 0.018 (34/500) Progress: 13600/200000 (6.8%) | Rate: 41.5 sim/s | ETA: 4491s N2/N1 = 0.019 (35/500) Progress: 14000/200000 (7.0%) | Rate: 41.5 sim/s | ETA: 4484s N2/N1 = 0.019 (36/500) Progress: 14400/200000 (7.2%) | Rate: 41.4 sim/s | ETA: 4478s N2/N1 = 0.019 (37/500) Progress: 14800/200000 (7.4%) | Rate: 41.4 sim/s | ETA: 4471s N2/N1 = 0.020 (38/500) Progress: 15200/200000 (7.6%) | Rate: 41.4 sim/s | ETA: 4465s N2/N1 = 0.020 (39/500) Progress: 15600/200000 (7.8%) | Rate: 41.3 sim/s | ETA: 4464s N2/N1 = 0.021 (40/500) Progress: 16000/200000 (8.0%) | Rate: 41.3 sim/s | ETA: 4457s N2/N1 = 0.021 (41/500) Progress: 16400/200000 (8.2%) | Rate: 41.2 sim/s | ETA: 4457s N2/N1 = 0.021 (42/500) Progress: 16800/200000 (8.4%) | Rate: 41.1 sim/s | ETA: 4455s N2/N1 = 0.022 (43/500) Progress: 17200/200000 (8.6%) | Rate: 41.1 sim/s | ETA: 4449s N2/N1 = 0.022 (44/500) Progress: 17600/200000 (8.8%) | Rate: 41.0 sim/s | ETA: 4444s N2/N1 = 0.023 (45/500) Progress: 18000/200000 (9.0%) | Rate: 41.0 sim/s | ETA: 4438s N2/N1 = 0.023 (46/500) Progress: 18400/200000 (9.2%) | Rate: 41.0 sim/s | ETA: 4431s N2/N1 = 0.023 (47/500) Progress: 18800/200000 (9.4%) | Rate: 41.0 sim/s | ETA: 4424s N2/N1 = 0.024 (48/500) Progress: 19200/200000 (9.6%) | Rate: 40.9 sim/s | ETA: 4418s N2/N1 = 0.024 (49/500) Progress: 19600/200000 (9.8%) | Rate: 40.9 sim/s | ETA: 4412s N2/N1 = 0.025 (50/500) Progress: 20000/200000 (10.0%) | Rate: 40.9 sim/s | ETA: 4406s N2/N1 = 0.025 (51/500) Progress: 20400/200000 (10.2%) | Rate: 40.8 sim/s | ETA: 4400s N2/N1 = 0.026 (52/500) Progress: 20800/200000 (10.4%) | Rate: 40.8 sim/s | ETA: 4394s N2/N1 = 0.026 (53/500) Progress: 21200/200000 (10.6%) | Rate: 40.8 sim/s | ETA: 4387s N2/N1 = 0.027 (54/500) Progress: 21600/200000 (10.8%) | Rate: 40.7 sim/s | ETA: 4381s N2/N1 = 0.027 (55/500) Progress: 22000/200000 (11.0%) | Rate: 40.7 sim/s | ETA: 4375s N2/N1 = 0.028 (56/500) Progress: 22400/200000 (11.2%) | Rate: 40.6 sim/s | ETA: 4369s N2/N1 = 0.028 (57/500) Progress: 22800/200000 (11.4%) | Rate: 40.6 sim/s | ETA: 4364s N2/N1 = 0.029 (58/500) Progress: 23200/200000 (11.6%) | Rate: 40.6 sim/s | ETA: 4359s N2/N1 = 0.029 (59/500) Progress: 23600/200000 (11.8%) | Rate: 40.5 sim/s | ETA: 4353s N2/N1 = 0.030 (60/500) Progress: 24000/200000 (12.0%) | Rate: 40.5 sim/s | ETA: 4347s N2/N1 = 0.030 (61/500) Progress: 24400/200000 (12.2%) | Rate: 40.4 sim/s | ETA: 4342s N2/N1 = 0.031 (62/500) Progress: 24800/200000 (12.4%) | Rate: 40.4 sim/s | ETA: 4336s N2/N1 = 0.031 (63/500) Progress: 25200/200000 (12.6%) | Rate: 40.4 sim/s | ETA: 4330s N2/N1 = 0.032 (64/500) Progress: 25600/200000 (12.8%) | Rate: 40.3 sim/s | ETA: 4324s N2/N1 = 0.033 (65/500) Progress: 26000/200000 (13.0%) | Rate: 40.3 sim/s | ETA: 4318s N2/N1 = 0.033 (66/500) Progress: 26400/200000 (13.2%) | Rate: 40.3 sim/s | ETA: 4312s N2/N1 = 0.034 (67/500) Progress: 26800/200000 (13.4%) | Rate: 40.2 sim/s | ETA: 4308s N2/N1 = 0.034 (68/500) Progress: 27200/200000 (13.6%) | Rate: 40.2 sim/s | ETA: 4302s N2/N1 = 0.035 (69/500) Progress: 27600/200000 (13.8%) | Rate: 40.1 sim/s | ETA: 4297s N2/N1 = 0.036 (70/500) Progress: 28000/200000 (14.0%) | Rate: 40.1 sim/s | ETA: 4292s N2/N1 = 0.036 (71/500) Progress: 28400/200000 (14.2%) | Rate: 40.0 sim/s | ETA: 4287s N2/N1 = 0.037 (72/500) Progress: 28800/200000 (14.4%) | Rate: 40.0 sim/s | ETA: 4281s N2/N1 = 0.038 (73/500) Progress: 29200/200000 (14.6%) | Rate: 40.0 sim/s | ETA: 4275s N2/N1 = 0.038 (74/500) Progress: 29600/200000 (14.8%) | Rate: 39.9 sim/s | ETA: 4269s N2/N1 = 0.039 (75/500) Progress: 30000/200000 (15.0%) | Rate: 39.9 sim/s | ETA: 4264s N2/N1 = 0.040 (76/500) Progress: 30400/200000 (15.2%) | Rate: 39.8 sim/s | ETA: 4258s N2/N1 = 0.041 (77/500) Progress: 30800/200000 (15.4%) | Rate: 39.8 sim/s | ETA: 4253s N2/N1 = 0.041 (78/500) Progress: 31200/200000 (15.6%) | Rate: 39.7 sim/s | ETA: 4248s N2/N1 = 0.042 (79/500) Progress: 31600/200000 (15.8%) | Rate: 39.7 sim/s | ETA: 4243s N2/N1 = 0.043 (80/500) Progress: 32000/200000 (16.0%) | Rate: 39.6 sim/s | ETA: 4237s N2/N1 = 0.044 (81/500) Progress: 32400/200000 (16.2%) | Rate: 39.6 sim/s | ETA: 4233s N2/N1 = 0.045 (82/500) Progress: 32800/200000 (16.4%) | Rate: 39.5 sim/s | ETA: 4228s N2/N1 = 0.045 (83/500) Progress: 33200/200000 (16.6%) | Rate: 39.5 sim/s | ETA: 4224s N2/N1 = 0.046 (84/500) Progress: 33600/200000 (16.8%) | Rate: 39.4 sim/s | ETA: 4219s N2/N1 = 0.047 (85/500) Progress: 34000/200000 (17.0%) | Rate: 39.4 sim/s | ETA: 4216s N2/N1 = 0.048 (86/500) Progress: 34400/200000 (17.2%) | Rate: 39.3 sim/s | ETA: 4212s N2/N1 = 0.049 (87/500) Progress: 34800/200000 (17.4%) | Rate: 39.3 sim/s | ETA: 4209s N2/N1 = 0.050 (88/500) Progress: 35200/200000 (17.6%) | Rate: 39.2 sim/s | ETA: 4206s N2/N1 = 0.051 (89/500) Progress: 35600/200000 (17.8%) | Rate: 39.1 sim/s | ETA: 4203s N2/N1 = 0.052 (90/500) Progress: 36000/200000 (18.0%) | Rate: 39.0 sim/s | ETA: 4201s N2/N1 = 0.053 (91/500) Progress: 36400/200000 (18.2%) | Rate: 39.0 sim/s | ETA: 4200s N2/N1 = 0.054 (92/500) Progress: 36800/200000 (18.4%) | Rate: 38.9 sim/s | ETA: 4200s N2/N1 = 0.055 (93/500) Progress: 37200/200000 (18.6%) | Rate: 38.8 sim/s | ETA: 4200s N2/N1 = 0.056 (94/500) Progress: 37600/200000 (18.8%) | Rate: 38.6 sim/s | ETA: 4202s N2/N1 = 0.057 (95/500) Progress: 38000/200000 (19.0%) | Rate: 38.5 sim/s | ETA: 4205s N2/N1 = 0.058 (96/500) Progress: 38400/200000 (19.2%) | Rate: 38.4 sim/s | ETA: 4208s N2/N1 = 0.059 (97/500) Progress: 38800/200000 (19.4%) | Rate: 38.3 sim/s | ETA: 4212s N2/N1 = 0.060 (98/500) Progress: 39200/200000 (19.6%) | Rate: 38.1 sim/s | ETA: 4216s N2/N1 = 0.061 (99/500) Progress: 39600/200000 (19.8%) | Rate: 38.0 sim/s | ETA: 4220s N2/N1 = 0.062 (100/500) Progress: 40000/200000 (20.0%) | Rate: 37.9 sim/s | ETA: 4224s N2/N1 = 0.063 (101/500) Progress: 40400/200000 (20.2%) | Rate: 37.7 sim/s | ETA: 4228s N2/N1 = 0.065 (102/500) Progress: 40800/200000 (20.4%) | Rate: 37.6 sim/s | ETA: 4232s N2/N1 = 0.066 (103/500) Progress: 41200/200000 (20.6%) | Rate: 37.5 sim/s | ETA: 4236s N2/N1 = 0.067 (104/500) Progress: 41600/200000 (20.8%) | Rate: 37.4 sim/s | ETA: 4240s N2/N1 = 0.068 (105/500) Progress: 42000/200000 (21.0%) | Rate: 37.2 sim/s | ETA: 4244s N2/N1 = 0.069 (106/500) Progress: 42400/200000 (21.2%) | Rate: 37.1 sim/s | ETA: 4248s N2/N1 = 0.071 (107/500) Progress: 42800/200000 (21.4%) | Rate: 37.0 sim/s | ETA: 4252s N2/N1 = 0.072 (108/500) Progress: 43200/200000 (21.6%) | Rate: 36.8 sim/s | ETA: 4257s N2/N1 = 0.073 (109/500) Progress: 43600/200000 (21.8%) | Rate: 36.7 sim/s | ETA: 4261s N2/N1 = 0.075 (110/500) Progress: 44000/200000 (22.0%) | Rate: 36.6 sim/s | ETA: 4264s N2/N1 = 0.076 (111/500) Progress: 44400/200000 (22.2%) | Rate: 36.5 sim/s | ETA: 4268s N2/N1 = 0.078 (112/500) Progress: 44800/200000 (22.4%) | Rate: 36.3 sim/s | ETA: 4272s N2/N1 = 0.079 (113/500) Progress: 45200/200000 (22.6%) | Rate: 36.2 sim/s | ETA: 4276s N2/N1 = 0.081 (114/500) Progress: 45600/200000 (22.8%) | Rate: 36.1 sim/s | ETA: 4279s N2/N1 = 0.082 (115/500) Progress: 46000/200000 (23.0%) | Rate: 36.0 sim/s | ETA: 4283s N2/N1 = 0.084 (116/500) Progress: 46400/200000 (23.2%) | Rate: 35.8 sim/s | ETA: 4287s N2/N1 = 0.085 (117/500) Progress: 46800/200000 (23.4%) | Rate: 35.7 sim/s | ETA: 4290s N2/N1 = 0.087 (118/500) Progress: 47200/200000 (23.6%) | Rate: 35.6 sim/s | ETA: 4294s N2/N1 = 0.088 (119/500) Progress: 47600/200000 (23.8%) | Rate: 35.5 sim/s | ETA: 4298s N2/N1 = 0.090 (120/500) Progress: 48000/200000 (24.0%) | Rate: 35.3 sim/s | ETA: 4301s N2/N1 = 0.092 (121/500) Progress: 48400/200000 (24.2%) | Rate: 35.2 sim/s | ETA: 4305s N2/N1 = 0.093 (122/500) Progress: 48800/200000 (24.4%) | Rate: 35.1 sim/s | ETA: 4309s N2/N1 = 0.095 (123/500) Progress: 49200/200000 (24.6%) | Rate: 35.0 sim/s | ETA: 4313s N2/N1 = 0.097 (124/500) Progress: 49600/200000 (24.8%) | Rate: 34.8 sim/s | ETA: 4317s N2/N1 = 0.099 (125/500) Progress: 50000/200000 (25.0%) | Rate: 34.7 sim/s | ETA: 4321s N2/N1 = 0.100 (126/500) Progress: 50400/200000 (25.2%) | Rate: 34.6 sim/s | ETA: 4326s N2/N1 = 0.102 (127/500) Progress: 50800/200000 (25.4%) | Rate: 34.5 sim/s | ETA: 4331s N2/N1 = 0.104 (128/500) Progress: 51200/200000 (25.6%) | Rate: 34.3 sim/s | ETA: 4335s N2/N1 = 0.106 (129/500) Progress: 51600/200000 (25.8%) | Rate: 34.2 sim/s | ETA: 4340s N2/N1 = 0.108 (130/500) Progress: 52000/200000 (26.0%) | Rate: 34.1 sim/s | ETA: 4346s N2/N1 = 0.110 (131/500) Progress: 52400/200000 (26.2%) | Rate: 33.9 sim/s | ETA: 4351s N2/N1 = 0.112 (132/500) Progress: 52800/200000 (26.4%) | Rate: 33.8 sim/s | ETA: 4357s N2/N1 = 0.114 (133/500) Progress: 53200/200000 (26.6%) | Rate: 33.6 sim/s | ETA: 4363s N2/N1 = 0.116 (134/500) Progress: 53600/200000 (26.8%) | Rate: 33.5 sim/s | ETA: 4369s N2/N1 = 0.119 (135/500) Progress: 54000/200000 (27.0%) | Rate: 33.4 sim/s | ETA: 4375s N2/N1 = 0.121 (136/500) Progress: 54400/200000 (27.2%) | Rate: 33.2 sim/s | ETA: 4383s N2/N1 = 0.123 (137/500) Progress: 54800/200000 (27.4%) | Rate: 33.1 sim/s | ETA: 4390s N2/N1 = 0.125 (138/500) Progress: 55200/200000 (27.6%) | Rate: 32.9 sim/s | ETA: 4397s N2/N1 = 0.128 (139/500) Progress: 55600/200000 (27.8%) | Rate: 32.8 sim/s | ETA: 4404s N2/N1 = 0.130 (140/500) Progress: 56000/200000 (28.0%) | Rate: 32.6 sim/s | ETA: 4412s N2/N1 = 0.133 (141/500) Progress: 56400/200000 (28.2%) | Rate: 32.5 sim/s | ETA: 4419s N2/N1 = 0.135 (142/500) Progress: 56800/200000 (28.4%) | Rate: 32.3 sim/s | ETA: 4427s N2/N1 = 0.137 (143/500) Progress: 57200/200000 (28.6%) | Rate: 32.2 sim/s | ETA: 4434s N2/N1 = 0.140 (144/500) Progress: 57600/200000 (28.8%) | Rate: 32.1 sim/s | ETA: 4442s N2/N1 = 0.143 (145/500) Progress: 58000/200000 (29.0%) | Rate: 31.9 sim/s | ETA: 4449s N2/N1 = 0.145 (146/500) Progress: 58400/200000 (29.2%) | Rate: 31.8 sim/s | ETA: 4457s N2/N1 = 0.148 (147/500) Progress: 58800/200000 (29.4%) | Rate: 31.6 sim/s | ETA: 4464s N2/N1 = 0.151 (148/500) Progress: 59200/200000 (29.6%) | Rate: 31.5 sim/s | ETA: 4471s N2/N1 = 0.154 (149/500) Progress: 59600/200000 (29.8%) | Rate: 31.4 sim/s | ETA: 4477s N2/N1 = 0.156 (150/500) Progress: 60000/200000 (30.0%) | Rate: 31.2 sim/s | ETA: 4485s N2/N1 = 0.159 (151/500) Progress: 60400/200000 (30.2%) | Rate: 31.1 sim/s | ETA: 4492s N2/N1 = 0.162 (152/500) Progress: 60800/200000 (30.4%) | Rate: 30.9 sim/s | ETA: 4499s N2/N1 = 0.165 (153/500) Progress: 61200/200000 (30.6%) | Rate: 30.8 sim/s | ETA: 4506s N2/N1 = 0.168 (154/500) Progress: 61600/200000 (30.8%) | Rate: 30.7 sim/s | ETA: 4513s N2/N1 = 0.172 (155/500) Progress: 62000/200000 (31.0%) | Rate: 30.5 sim/s | ETA: 4519s N2/N1 = 0.175 (156/500) Progress: 62400/200000 (31.2%) | Rate: 30.4 sim/s | ETA: 4525s N2/N1 = 0.178 (157/500) Progress: 62800/200000 (31.4%) | Rate: 30.3 sim/s | ETA: 4531s N2/N1 = 0.181 (158/500) Progress: 63200/200000 (31.6%) | Rate: 30.1 sim/s | ETA: 4538s N2/N1 = 0.185 (159/500) Progress: 63600/200000 (31.8%) | Rate: 30.0 sim/s | ETA: 4544s N2/N1 = 0.188 (160/500) Progress: 64000/200000 (32.0%) | Rate: 29.9 sim/s | ETA: 4551s N2/N1 = 0.192 (161/500) Progress: 64400/200000 (32.2%) | Rate: 29.8 sim/s | ETA: 4558s N2/N1 = 0.195 (162/500) Progress: 64800/200000 (32.4%) | Rate: 29.6 sim/s | ETA: 4565s N2/N1 = 0.199 (163/500) Progress: 65200/200000 (32.6%) | Rate: 29.5 sim/s | ETA: 4572s N2/N1 = 0.203 (164/500) Progress: 65600/200000 (32.8%) | Rate: 29.4 sim/s | ETA: 4579s N2/N1 = 0.206 (165/500) Progress: 66000/200000 (33.0%) | Rate: 29.2 sim/s | ETA: 4586s N2/N1 = 0.210 (166/500) Progress: 66400/200000 (33.2%) | Rate: 29.1 sim/s | ETA: 4593s N2/N1 = 0.214 (167/500) Progress: 66800/200000 (33.4%) | Rate: 29.0 sim/s | ETA: 4600s N2/N1 = 0.218 (168/500) Progress: 67200/200000 (33.6%) | Rate: 28.8 sim/s | ETA: 4608s N2/N1 = 0.222 (169/500) Progress: 67600/200000 (33.8%) | Rate: 28.7 sim/s | ETA: 4616s N2/N1 = 0.226 (170/500) Progress: 68000/200000 (34.0%) | Rate: 28.5 sim/s | ETA: 4624s N2/N1 = 0.231 (171/500) Progress: 68400/200000 (34.2%) | Rate: 28.4 sim/s | ETA: 4633s N2/N1 = 0.235 (172/500) Progress: 68800/200000 (34.4%) | Rate: 28.3 sim/s | ETA: 4642s N2/N1 = 0.239 (173/500) Progress: 69200/200000 (34.6%) | Rate: 28.1 sim/s | ETA: 4650s N2/N1 = 0.244 (174/500) Progress: 69600/200000 (34.8%) | Rate: 28.0 sim/s | ETA: 4659s N2/N1 = 0.248 (175/500) Progress: 70000/200000 (35.0%) | Rate: 27.8 sim/s | ETA: 4670s N2/N1 = 0.253 (176/500) Progress: 70400/200000 (35.2%) | Rate: 27.7 sim/s | ETA: 4680s N2/N1 = 0.258 (177/500) Progress: 70800/200000 (35.4%) | Rate: 27.5 sim/s | ETA: 4691s N2/N1 = 0.262 (178/500) Progress: 71200/200000 (35.6%) | Rate: 27.4 sim/s | ETA: 4702s N2/N1 = 0.267 (179/500) Progress: 71600/200000 (35.8%) | Rate: 27.2 sim/s | ETA: 4714s N2/N1 = 0.272 (180/500) Progress: 72000/200000 (36.0%) | Rate: 27.1 sim/s | ETA: 4726s N2/N1 = 0.277 (181/500) Progress: 72400/200000 (36.2%) | Rate: 26.9 sim/s | ETA: 4737s N2/N1 = 0.282 (182/500) Progress: 72800/200000 (36.4%) | Rate: 26.8 sim/s | ETA: 4749s N2/N1 = 0.288 (183/500) Progress: 73200/200000 (36.6%) | Rate: 26.6 sim/s | ETA: 4760s N2/N1 = 0.293 (184/500) Progress: 73600/200000 (36.8%) | Rate: 26.5 sim/s | ETA: 4772s N2/N1 = 0.299 (185/500) Progress: 74000/200000 (37.0%) | Rate: 26.3 sim/s | ETA: 4783s N2/N1 = 0.304 (186/500) Progress: 74400/200000 (37.2%) | Rate: 26.2 sim/s | ETA: 4794s N2/N1 = 0.310 (187/500) Progress: 74800/200000 (37.4%) | Rate: 26.1 sim/s | ETA: 4805s N2/N1 = 0.315 (188/500) Progress: 75200/200000 (37.6%) | Rate: 25.9 sim/s | ETA: 4816s N2/N1 = 0.321 (189/500) Progress: 75600/200000 (37.8%) | Rate: 25.8 sim/s | ETA: 4828s N2/N1 = 0.327 (190/500) Progress: 76000/200000 (38.0%) | Rate: 25.6 sim/s | ETA: 4840s N2/N1 = 0.333 (191/500) Progress: 76400/200000 (38.2%) | Rate: 25.5 sim/s | ETA: 4852s N2/N1 = 0.340 (192/500) Progress: 76800/200000 (38.4%) | Rate: 25.3 sim/s | ETA: 4865s N2/N1 = 0.346 (193/500) Progress: 77200/200000 (38.6%) | Rate: 25.2 sim/s | ETA: 4878s N2/N1 = 0.352 (194/500) Progress: 77600/200000 (38.8%) | Rate: 25.0 sim/s | ETA: 4891s N2/N1 = 0.359 (195/500) Progress: 78000/200000 (39.0%) | Rate: 24.9 sim/s | ETA: 4905s N2/N1 = 0.366 (196/500) Progress: 78400/200000 (39.2%) | Rate: 24.7 sim/s | ETA: 4919s N2/N1 = 0.373 (197/500) Progress: 78800/200000 (39.4%) | Rate: 24.6 sim/s | ETA: 4933s N2/N1 = 0.379 (198/500) Progress: 79200/200000 (39.6%) | Rate: 24.4 sim/s | ETA: 4947s N2/N1 = 0.387 (199/500) Progress: 79600/200000 (39.8%) | Rate: 24.3 sim/s | ETA: 4962s N2/N1 = 0.394 (200/500) Progress: 80000/200000 (40.0%) | Rate: 24.1 sim/s | ETA: 4977s N2/N1 = 0.401 (201/500) Progress: 80400/200000 (40.2%) | Rate: 24.0 sim/s | ETA: 4993s N2/N1 = 0.409 (202/500) Progress: 80800/200000 (40.4%) | Rate: 23.8 sim/s | ETA: 5008s N2/N1 = 0.416 (203/500) Progress: 81200/200000 (40.6%) | Rate: 23.6 sim/s | ETA: 5024s N2/N1 = 0.424 (204/500) Progress: 81600/200000 (40.8%) | Rate: 23.5 sim/s | ETA: 5040s N2/N1 = 0.432 (205/500) Progress: 82000/200000 (41.0%) | Rate: 23.3 sim/s | ETA: 5056s N2/N1 = 0.440 (206/500) Progress: 82400/200000 (41.2%) | Rate: 23.2 sim/s | ETA: 5072s N2/N1 = 0.448 (207/500) Progress: 82800/200000 (41.4%) | Rate: 23.0 sim/s | ETA: 5089s N2/N1 = 0.456 (208/500) Progress: 83200/200000 (41.6%) | Rate: 22.9 sim/s | ETA: 5108s N2/N1 = 0.465 (209/500) Progress: 83600/200000 (41.8%) | Rate: 22.7 sim/s | ETA: 5127s N2/N1 = 0.474 (210/500) Progress: 84000/200000 (42.0%) | Rate: 22.5 sim/s | ETA: 5146s N2/N1 = 0.482 (211/500) Progress: 84400/200000 (42.2%) | Rate: 22.4 sim/s | ETA: 5165s N2/N1 = 0.491 (212/500) Progress: 84800/200000 (42.4%) | Rate: 22.2 sim/s | ETA: 5185s N2/N1 = 0.500 (213/500) Progress: 85200/200000 (42.6%) | Rate: 22.1 sim/s | ETA: 5204s N2/N1 = 0.510 (214/500) Progress: 85600/200000 (42.8%) | Rate: 21.9 sim/s | ETA: 5224s N2/N1 = 0.519 (215/500) Progress: 86000/200000 (43.0%) | Rate: 21.7 sim/s | ETA: 5245s N2/N1 = 0.529 (216/500) Progress: 86400/200000 (43.2%) | Rate: 21.6 sim/s | ETA: 5266s N2/N1 = 0.539 (217/500) Progress: 86800/200000 (43.4%) | Rate: 21.4 sim/s | ETA: 5288s N2/N1 = 0.549 (218/500) Progress: 87200/200000 (43.6%) | Rate: 21.2 sim/s | ETA: 5310s N2/N1 = 0.559 (219/500) Progress: 87600/200000 (43.8%) | Rate: 21.1 sim/s | ETA: 5332s N2/N1 = 0.570 (220/500) Progress: 88000/200000 (44.0%) | Rate: 20.9 sim/s | ETA: 5355s N2/N1 = 0.580 (221/500) Progress: 88400/200000 (44.2%) | Rate: 20.7 sim/s | ETA: 5379s N2/N1 = 0.591 (222/500) Progress: 88800/200000 (44.4%) | Rate: 20.6 sim/s | ETA: 5403s N2/N1 = 0.602 (223/500) Progress: 89200/200000 (44.6%) | Rate: 20.4 sim/s | ETA: 5427s N2/N1 = 0.613 (224/500) Progress: 89600/200000 (44.8%) | Rate: 20.2 sim/s | ETA: 5452s N2/N1 = 0.625 (225/500) Progress: 90000/200000 (45.0%) | Rate: 20.1 sim/s | ETA: 5478s N2/N1 = 0.636 (226/500) Progress: 90400/200000 (45.2%) | Rate: 19.9 sim/s | ETA: 5505s N2/N1 = 0.648 (227/500) Progress: 90800/200000 (45.4%) | Rate: 19.7 sim/s | ETA: 5532s N2/N1 = 0.660 (228/500) Progress: 91200/200000 (45.6%) | Rate: 19.6 sim/s | ETA: 5559s N2/N1 = 0.672 (229/500) Progress: 91600/200000 (45.8%) | Rate: 19.4 sim/s | ETA: 5587s N2/N1 = 0.685 (230/500) Progress: 92000/200000 (46.0%) | Rate: 19.2 sim/s | ETA: 5616s N2/N1 = 0.698 (231/500) Progress: 92400/200000 (46.2%) | Rate: 19.1 sim/s | ETA: 5645s N2/N1 = 0.711 (232/500) Progress: 92800/200000 (46.4%) | Rate: 18.9 sim/s | ETA: 5676s N2/N1 = 0.724 (233/500) Progress: 93200/200000 (46.6%) | Rate: 18.7 sim/s | ETA: 5706s N2/N1 = 0.737 (234/500) Progress: 93600/200000 (46.8%) | Rate: 18.5 sim/s | ETA: 5738s N2/N1 = 0.751 (235/500) Progress: 94000/200000 (47.0%) | Rate: 18.4 sim/s | ETA: 5772s N2/N1 = 0.765 (236/500) Progress: 94400/200000 (47.2%) | Rate: 18.2 sim/s | ETA: 5805s N2/N1 = 0.779 (237/500) Progress: 94800/200000 (47.4%) | Rate: 18.0 sim/s | ETA: 5837s N2/N1 = 0.794 (238/500) Progress: 95200/200000 (47.6%) | Rate: 17.9 sim/s | ETA: 5868s N2/N1 = 0.809 (239/500) Progress: 95600/200000 (47.8%) | Rate: 17.7 sim/s | ETA: 5896s N2/N1 = 0.824 (240/500) Progress: 96000/200000 (48.0%) | Rate: 17.6 sim/s | ETA: 5925s N2/N1 = 0.839 (241/500) Progress: 96400/200000 (48.2%) | Rate: 17.4 sim/s | ETA: 5953s N2/N1 = 0.855 (242/500) Progress: 96800/200000 (48.4%) | Rate: 17.3 sim/s | ETA: 5980s N2/N1 = 0.871 (243/500) Progress: 97200/200000 (48.6%) | Rate: 17.1 sim/s | ETA: 6007s N2/N1 = 0.887 (244/500) Progress: 97600/200000 (48.8%) | Rate: 17.0 sim/s | ETA: 6036s N2/N1 = 0.903 (245/500) Progress: 98000/200000 (49.0%) | Rate: 16.8 sim/s | ETA: 6062s N2/N1 = 0.920 (246/500) Progress: 98400/200000 (49.2%) | Rate: 16.7 sim/s | ETA: 6087s N2/N1 = 0.937 (247/500) Progress: 98800/200000 (49.4%) | Rate: 16.6 sim/s | ETA: 6113s N2/N1 = 0.955 (248/500) Progress: 99200/200000 (49.6%) | Rate: 16.4 sim/s | ETA: 6138s N2/N1 = 0.973 (249/500) Progress: 99600/200000 (49.8%) | Rate: 16.3 sim/s | ETA: 6163s N2/N1 = 0.991 (250/500) Progress: 100000/200000 (50.0%) | Rate: 16.2 sim/s | ETA: 6188s N2/N1 = 1.009 (251/500) Progress: 100400/200000 (50.2%) | Rate: 16.0 sim/s | ETA: 6213s N2/N1 = 1.028 (252/500) Progress: 100800/200000 (50.4%) | Rate: 15.9 sim/s | ETA: 6236s N2/N1 = 1.047 (253/500) Progress: 101200/200000 (50.6%) | Rate: 15.8 sim/s | ETA: 6260s N2/N1 = 1.067 (254/500) Progress: 101600/200000 (50.8%) | Rate: 15.7 sim/s | ETA: 6284s N2/N1 = 1.087 (255/500) Progress: 102000/200000 (51.0%) | Rate: 15.5 sim/s | ETA: 6307s N2/N1 = 1.107 (256/500) Progress: 102400/200000 (51.2%) | Rate: 15.4 sim/s | ETA: 6329s N2/N1 = 1.127 (257/500) Progress: 102800/200000 (51.4%) | Rate: 15.3 sim/s | ETA: 6352s N2/N1 = 1.148 (258/500) Progress: 103200/200000 (51.6%) | Rate: 15.2 sim/s | ETA: 6374s N2/N1 = 1.170 (259/500) Progress: 103600/200000 (51.8%) | Rate: 15.1 sim/s | ETA: 6397s N2/N1 = 1.192 (260/500) Progress: 104000/200000 (52.0%) | Rate: 15.0 sim/s | ETA: 6419s N2/N1 = 1.214 (261/500) Progress: 104400/200000 (52.2%) | Rate: 14.8 sim/s | ETA: 6440s N2/N1 = 1.236 (262/500) Progress: 104800/200000 (52.4%) | Rate: 14.7 sim/s | ETA: 6462s N2/N1 = 1.260 (263/500) Progress: 105200/200000 (52.6%) | Rate: 14.6 sim/s | ETA: 6482s N2/N1 = 1.283 (264/500) Progress: 105600/200000 (52.8%) | Rate: 14.5 sim/s | ETA: 6503s N2/N1 = 1.307 (265/500) Progress: 106000/200000 (53.0%) | Rate: 14.4 sim/s | ETA: 6523s N2/N1 = 1.331 (266/500) Progress: 106400/200000 (53.2%) | Rate: 14.3 sim/s | ETA: 6543s N2/N1 = 1.356 (267/500) Progress: 106800/200000 (53.4%) | Rate: 14.2 sim/s | ETA: 6562s N2/N1 = 1.381 (268/500) Progress: 107200/200000 (53.6%) | Rate: 14.1 sim/s | ETA: 6582s N2/N1 = 1.407 (269/500) Progress: 107600/200000 (53.8%) | Rate: 14.0 sim/s | ETA: 6601s N2/N1 = 1.433 (270/500) Progress: 108000/200000 (54.0%) | Rate: 13.9 sim/s | ETA: 6619s N2/N1 = 1.460 (271/500) Progress: 108400/200000 (54.2%) | Rate: 13.8 sim/s | ETA: 6639s N2/N1 = 1.487 (272/500) Progress: 108800/200000 (54.4%) | Rate: 13.7 sim/s | ETA: 6657s N2/N1 = 1.515 (273/500) Progress: 109200/200000 (54.6%) | Rate: 13.6 sim/s | ETA: 6675s N2/N1 = 1.543 (274/500) Progress: 109600/200000 (54.8%) | Rate: 13.5 sim/s | ETA: 6693s N2/N1 = 1.572 (275/500) Progress: 110000/200000 (55.0%) | Rate: 13.4 sim/s | ETA: 6711s N2/N1 = 1.601 (276/500) Progress: 110400/200000 (55.2%) | Rate: 13.3 sim/s | ETA: 6728s N2/N1 = 1.631 (277/500) Progress: 110800/200000 (55.4%) | Rate: 13.2 sim/s | ETA: 6750s N2/N1 = 1.661 (278/500) Progress: 111200/200000 (55.6%) | Rate: 13.1 sim/s | ETA: 6767s N2/N1 = 1.692 (279/500) Progress: 111600/200000 (55.8%) | Rate: 13.0 sim/s | ETA: 6783s N2/N1 = 1.724 (280/500) Progress: 112000/200000 (56.0%) | Rate: 12.9 sim/s | ETA: 6799s N2/N1 = 1.756 (281/500) Progress: 112400/200000 (56.2%) | Rate: 12.9 sim/s | ETA: 6815s N2/N1 = 1.789 (282/500) Progress: 112800/200000 (56.4%) | Rate: 12.8 sim/s | ETA: 6831s N2/N1 = 1.822 (283/500) Progress: 113200/200000 (56.6%) | Rate: 12.7 sim/s | ETA: 6846s N2/N1 = 1.856 (284/500) Progress: 113600/200000 (56.8%) | Rate: 12.6 sim/s | ETA: 6861s N2/N1 = 1.890 (285/500) Progress: 114000/200000 (57.0%) | Rate: 12.5 sim/s | ETA: 6875s N2/N1 = 1.926 (286/500) Progress: 114400/200000 (57.2%) | Rate: 12.4 sim/s | ETA: 6889s N2/N1 = 1.961 (287/500) Progress: 114800/200000 (57.4%) | Rate: 12.3 sim/s | ETA: 6907s N2/N1 = 1.998 (288/500) Progress: 115200/200000 (57.6%) | Rate: 12.3 sim/s | ETA: 6920s N2/N1 = 2.035 (289/500) Progress: 115600/200000 (57.8%) | Rate: 12.2 sim/s | ETA: 6933s N2/N1 = 2.073 (290/500) Progress: 116000/200000 (58.0%) | Rate: 12.1 sim/s | ETA: 6946s N2/N1 = 2.112 (291/500) Progress: 116400/200000 (58.2%) | Rate: 12.0 sim/s | ETA: 6959s N2/N1 = 2.151 (292/500) Progress: 116800/200000 (58.4%) | Rate: 11.9 sim/s | ETA: 6971s N2/N1 = 2.191 (293/500) Progress: 117200/200000 (58.6%) | Rate: 11.9 sim/s | ETA: 6983s N2/N1 = 2.232 (294/500) Progress: 117600/200000 (58.8%) | Rate: 11.8 sim/s | ETA: 6994s N2/N1 = 2.274 (295/500) Progress: 118000/200000 (59.0%) | Rate: 11.7 sim/s | ETA: 7005s N2/N1 = 2.316 (296/500) Progress: 118400/200000 (59.2%) | Rate: 11.6 sim/s | ETA: 7015s N2/N1 = 2.359 (297/500) Progress: 118800/200000 (59.4%) | Rate: 11.6 sim/s | ETA: 7025s N2/N1 = 2.403 (298/500) Progress: 119200/200000 (59.6%) | Rate: 11.5 sim/s | ETA: 7036s N2/N1 = 2.448 (299/500) Progress: 119600/200000 (59.8%) | Rate: 11.4 sim/s | ETA: 7046s N2/N1 = 2.493 (300/500) Progress: 120000/200000 (60.0%) | Rate: 11.3 sim/s | ETA: 7055s N2/N1 = 2.540 (301/500) Progress: 120400/200000 (60.2%) | Rate: 11.3 sim/s | ETA: 7064s N2/N1 = 2.587 (302/500) Progress: 120800/200000 (60.4%) | Rate: 11.2 sim/s | ETA: 7072s N2/N1 = 2.635 (303/500) Progress: 121200/200000 (60.6%) | Rate: 11.1 sim/s | ETA: 7080s N2/N1 = 2.684 (304/500) Progress: 121600/200000 (60.8%) | Rate: 11.1 sim/s | ETA: 7088s N2/N1 = 2.734 (305/500) Progress: 122000/200000 (61.0%) | Rate: 11.0 sim/s | ETA: 7095s N2/N1 = 2.785 (306/500) Progress: 122400/200000 (61.2%) | Rate: 10.9 sim/s | ETA: 7103s N2/N1 = 2.837 (307/500) Progress: 122800/200000 (61.4%) | Rate: 10.9 sim/s | ETA: 7111s N2/N1 = 2.890 (308/500) Progress: 123200/200000 (61.6%) | Rate: 10.8 sim/s | ETA: 7118s N2/N1 = 2.944 (309/500) Progress: 123600/200000 (61.8%) | Rate: 10.7 sim/s | ETA: 7124s N2/N1 = 2.999 (310/500) Progress: 124000/200000 (62.0%) | Rate: 10.7 sim/s | ETA: 7130s N2/N1 = 3.055 (311/500) Progress: 124400/200000 (62.2%) | Rate: 10.6 sim/s | ETA: 7135s N2/N1 = 3.112 (312/500) Progress: 124800/200000 (62.4%) | Rate: 10.5 sim/s | ETA: 7140s N2/N1 = 3.170 (313/500) Progress: 125200/200000 (62.6%) | Rate: 10.5 sim/s | ETA: 7145s N2/N1 = 3.229 (314/500) Progress: 125600/200000 (62.8%) | Rate: 10.4 sim/s | ETA: 7149s N2/N1 = 3.289 (315/500) Progress: 126000/200000 (63.0%) | Rate: 10.3 sim/s | ETA: 7152s N2/N1 = 3.350 (316/500) Progress: 126400/200000 (63.2%) | Rate: 10.3 sim/s | ETA: 7163s N2/N1 = 3.412 (317/500) Progress: 126800/200000 (63.4%) | Rate: 10.2 sim/s | ETA: 7166s N2/N1 = 3.476 (318/500) Progress: 127200/200000 (63.6%) | Rate: 10.2 sim/s | ETA: 7169s N2/N1 = 3.541 (319/500) Progress: 127600/200000 (63.8%) | Rate: 10.1 sim/s | ETA: 7169s N2/N1 = 3.607 (320/500) Progress: 128000/200000 (64.0%) | Rate: 10.0 sim/s | ETA: 7170s N2/N1 = 3.674 (321/500) Progress: 128400/200000 (64.2%) | Rate: 10.0 sim/s | ETA: 7170s N2/N1 = 3.742 (322/500) Progress: 128800/200000 (64.4%) | Rate: 9.9 sim/s | ETA: 7171ss N2/N1 = 3.812 (323/500) Progress: 129200/200000 (64.6%) | Rate: 9.9 sim/s | ETA: 7170s N2/N1 = 3.883 (324/500) Progress: 129600/200000 (64.8%) | Rate: 9.8 sim/s | ETA: 7170s N2/N1 = 3.955 (325/500) Progress: 130000/200000 (65.0%) | Rate: 9.8 sim/s | ETA: 7169s N2/N1 = 4.029 (326/500) Progress: 130400/200000 (65.2%) | Rate: 9.7 sim/s | ETA: 7168s N2/N1 = 4.104 (327/500) Progress: 130800/200000 (65.4%) | Rate: 9.7 sim/s | ETA: 7167s N2/N1 = 4.181 (328/500) Progress: 131200/200000 (65.6%) | Rate: 9.6 sim/s | ETA: 7166s N2/N1 = 4.259 (329/500) Progress: 131600/200000 (65.8%) | Rate: 9.5 sim/s | ETA: 7163s N2/N1 = 4.338 (330/500) Progress: 132000/200000 (66.0%) | Rate: 9.5 sim/s | ETA: 7161s N2/N1 = 4.419 (331/500) Progress: 132400/200000 (66.2%) | Rate: 9.4 sim/s | ETA: 7157s N2/N1 = 4.501 (332/500) Progress: 132800/200000 (66.4%) | Rate: 9.4 sim/s | ETA: 7156s N2/N1 = 4.585 (333/500) Progress: 133200/200000 (66.6%) | Rate: 9.3 sim/s | ETA: 7152s N2/N1 = 4.670 (334/500) Progress: 133600/200000 (66.8%) | Rate: 9.3 sim/s | ETA: 7148s N2/N1 = 4.757 (335/500) Progress: 134000/200000 (67.0%) | Rate: 9.2 sim/s | ETA: 7144s N2/N1 = 4.846 (336/500) Progress: 134400/200000 (67.2%) | Rate: 9.2 sim/s | ETA: 7139s N2/N1 = 4.936 (337/500) Progress: 134800/200000 (67.4%) | Rate: 9.1 sim/s | ETA: 7133s N2/N1 = 5.028 (338/500) Progress: 135200/200000 (67.6%) | Rate: 9.1 sim/s | ETA: 7128s N2/N1 = 5.122 (339/500) Progress: 135600/200000 (67.8%) | Rate: 9.0 sim/s | ETA: 7123s N2/N1 = 5.217 (340/500) Progress: 136000/200000 (68.0%) | Rate: 9.0 sim/s | ETA: 7119s N2/N1 = 5.314 (341/500) Progress: 136400/200000 (68.2%) | Rate: 8.9 sim/s | ETA: 7115s N2/N1 = 5.413 (342/500) Progress: 136800/200000 (68.4%) | Rate: 8.9 sim/s | ETA: 7112s N2/N1 = 5.514 (343/500) Progress: 137200/200000 (68.6%) | Rate: 8.8 sim/s | ETA: 7109s N2/N1 = 5.617 (344/500) Progress: 137600/200000 (68.8%) | Rate: 8.8 sim/s | ETA: 7107s N2/N1 = 5.722 (345/500) Progress: 138000/200000 (69.0%) | Rate: 8.7 sim/s | ETA: 7119s N2/N1 = 5.828 (346/500) Progress: 138400/200000 (69.2%) | Rate: 8.7 sim/s | ETA: 7116s N2/N1 = 5.937 (347/500) Progress: 138800/200000 (69.4%) | Rate: 8.6 sim/s | ETA: 7114s N2/N1 = 6.047 (348/500) Progress: 139200/200000 (69.6%) | Rate: 8.5 sim/s | ETA: 7111s N2/N1 = 6.160 (349/500) Progress: 139600/200000 (69.8%) | Rate: 8.5 sim/s | ETA: 7109s N2/N1 = 6.275 (350/500) Progress: 140000/200000 (70.0%) | Rate: 8.4 sim/s | ETA: 7108s N2/N1 = 6.392 (351/500) Progress: 140400/200000 (70.2%) | Rate: 8.4 sim/s | ETA: 7106s N2/N1 = 6.511 (352/500) Progress: 140800/200000 (70.4%) | Rate: 8.3 sim/s | ETA: 7104s N2/N1 = 6.632 (353/500) Progress: 141200/200000 (70.6%) | Rate: 8.3 sim/s | ETA: 7103s N2/N1 = 6.756 (354/500) Progress: 141600/200000 (70.8%) | Rate: 8.2 sim/s | ETA: 7101s N2/N1 = 6.881 (355/500) Progress: 142000/200000 (71.0%) | Rate: 8.2 sim/s | ETA: 7100s N2/N1 = 7.010 (356/500) Progress: 142400/200000 (71.2%) | Rate: 8.1 sim/s | ETA: 7099s N2/N1 = 7.140 (357/500) Progress: 142800/200000 (71.4%) | Rate: 8.1 sim/s | ETA: 7098s N2/N1 = 7.273 (358/500) Progress: 143200/200000 (71.6%) | Rate: 8.0 sim/s | ETA: 7097s N2/N1 = 7.409 (359/500) Progress: 143600/200000 (71.8%) | Rate: 7.9 sim/s | ETA: 7097s N2/N1 = 7.547 (360/500) Progress: 144000/200000 (72.0%) | Rate: 7.9 sim/s | ETA: 7096s N2/N1 = 7.687 (361/500) Progress: 144400/200000 (72.2%) | Rate: 7.8 sim/s | ETA: 7095s N2/N1 = 7.830 (362/500) Progress: 144800/200000 (72.4%) | Rate: 7.8 sim/s | ETA: 7093s N2/N1 = 7.976 (363/500) Progress: 145200/200000 (72.6%) | Rate: 7.7 sim/s | ETA: 7092s N2/N1 = 8.125 (364/500) Progress: 145600/200000 (72.8%) | Rate: 7.7 sim/s | ETA: 7091s N2/N1 = 8.276 (365/500) Progress: 146000/200000 (73.0%) | Rate: 7.6 sim/s | ETA: 7089s N2/N1 = 8.430 (366/500) Progress: 146400/200000 (73.2%) | Rate: 7.6 sim/s | ETA: 7087s N2/N1 = 8.588 (367/500) Progress: 146800/200000 (73.4%) | Rate: 7.5 sim/s | ETA: 7085s N2/N1 = 8.747 (368/500) Progress: 147200/200000 (73.6%) | Rate: 7.5 sim/s | ETA: 7083s N2/N1 = 8.910 (369/500) Progress: 147600/200000 (73.8%) | Rate: 7.4 sim/s | ETA: 7080s N2/N1 = 9.076 (370/500) Progress: 148000/200000 (74.0%) | Rate: 7.3 sim/s | ETA: 7078s N2/N1 = 9.246 (371/500) Progress: 148400/200000 (74.2%) | Rate: 7.3 sim/s | ETA: 7074s N2/N1 = 9.418 (372/500) Progress: 148800/200000 (74.4%) | Rate: 7.2 sim/s | ETA: 7072s N2/N1 = 9.593 (373/500) Progress: 149200/200000 (74.6%) | Rate: 7.2 sim/s | ETA: 7069s N2/N1 = 9.772 (374/500) Progress: 149600/200000 (74.8%) | Rate: 7.1 sim/s | ETA: 7065s N2/N1 = 9.954 (375/500) Progress: 150000/200000 (75.0%) | Rate: 7.1 sim/s | ETA: 7062s N2/N1 = 10.139 (376/500) Progress: 150400/200000 (75.2%) | Rate: 7.0 sim/s | ETA: 7058s N2/N1 = 10.328 (377/500) Progress: 150800/200000 (75.4%) | Rate: 7.0 sim/s | ETA: 7053s N2/N1 = 10.521 (378/500) Progress: 151200/200000 (75.6%) | Rate: 6.9 sim/s | ETA: 7049s N2/N1 = 10.717 (379/500) Progress: 151600/200000 (75.8%) | Rate: 6.9 sim/s | ETA: 7044s N2/N1 = 10.916 (380/500) Progress: 152000/200000 (76.0%) | Rate: 6.8 sim/s | ETA: 7039s N2/N1 = 11.120 (381/500) Progress: 152400/200000 (76.2%) | Rate: 6.8 sim/s | ETA: 7034s N2/N1 = 11.327 (382/500) Progress: 152800/200000 (76.4%) | Rate: 6.7 sim/s | ETA: 7028s N2/N1 = 11.538 (383/500) Progress: 153200/200000 (76.6%) | Rate: 6.7 sim/s | ETA: 7022s N2/N1 = 11.753 (384/500) Progress: 153600/200000 (76.8%) | Rate: 6.6 sim/s | ETA: 7016s N2/N1 = 11.972 (385/500) Progress: 154000/200000 (77.0%) | Rate: 6.6 sim/s | ETA: 7009s N2/N1 = 12.195 (386/500) Progress: 154400/200000 (77.2%) | Rate: 6.5 sim/s | ETA: 7002s N2/N1 = 12.422 (387/500) Progress: 154800/200000 (77.4%) | Rate: 6.5 sim/s | ETA: 6995s N2/N1 = 12.653 (388/500) Progress: 155200/200000 (77.6%) | Rate: 6.4 sim/s | ETA: 6987s N2/N1 = 12.889 (389/500) Progress: 155600/200000 (77.8%) | Rate: 6.4 sim/s | ETA: 6979s N2/N1 = 13.129 (390/500) Progress: 156000/200000 (78.0%) | Rate: 6.3 sim/s | ETA: 6971s N2/N1 = 13.374 (391/500) Progress: 156400/200000 (78.2%) | Rate: 6.3 sim/s | ETA: 6964s N2/N1 = 13.623 (392/500) Progress: 156800/200000 (78.4%) | Rate: 6.2 sim/s | ETA: 6954s N2/N1 = 13.877 (393/500) Progress: 157200/200000 (78.6%) | Rate: 6.2 sim/s | ETA: 6944s N2/N1 = 14.135 (394/500) Progress: 157600/200000 (78.8%) | Rate: 6.1 sim/s | ETA: 6934s N2/N1 = 14.398 (395/500) Progress: 158000/200000 (79.0%) | Rate: 6.1 sim/s | ETA: 6923s N2/N1 = 14.667 (396/500) Progress: 158400/200000 (79.2%) | Rate: 6.0 sim/s | ETA: 6911s N2/N1 = 14.940 (397/500) Progress: 158800/200000 (79.4%) | Rate: 6.0 sim/s | ETA: 6899s N2/N1 = 15.218 (398/500) Progress: 159200/200000 (79.6%) | Rate: 5.9 sim/s | ETA: 6885s N2/N1 = 15.502 (399/500) Progress: 159600/200000 (79.8%) | Rate: 5.9 sim/s | ETA: 6872s N2/N1 = 15.791 (400/500) Progress: 160000/200000 (80.0%) | Rate: 5.7 sim/s | ETA: 7020s N2/N1 = 16.085 (401/500) Progress: 160400/200000 (80.2%) | Rate: 5.7 sim/s | ETA: 7004s N2/N1 = 16.384 (402/500) Progress: 160800/200000 (80.4%) | Rate: 5.6 sim/s | ETA: 6987s N2/N1 = 16.690 (403/500) Progress: 161200/200000 (80.6%) | Rate: 5.6 sim/s | ETA: 6969s N2/N1 = 17.000 (404/500) Progress: 161600/200000 (80.8%) | Rate: 5.4 sim/s | ETA: 7166s N2/N1 = 17.317 (405/500) Progress: 162000/200000 (81.0%) | Rate: 5.3 sim/s | ETA: 7145s N2/N1 = 17.640 (406/500) Progress: 162400/200000 (81.2%) | Rate: 5.3 sim/s | ETA: 7125s N2/N1 = 17.968 (407/500) Progress: 162800/200000 (81.4%) | Rate: 5.2 sim/s | ETA: 7103s N2/N1 = 18.303 (408/500) Progress: 163200/200000 (81.6%) | Rate: 5.2 sim/s | ETA: 7080s N2/N1 = 18.644 (409/500) Progress: 163600/200000 (81.8%) | Rate: 5.2 sim/s | ETA: 7057s N2/N1 = 18.991 (410/500) Progress: 164000/200000 (82.0%) | Rate: 5.1 sim/s | ETA: 7034s N2/N1 = 19.345 (411/500) Progress: 164400/200000 (82.2%) | Rate: 5.1 sim/s | ETA: 7010s N2/N1 = 19.706 (412/500) Progress: 164800/200000 (82.4%) | Rate: 5.0 sim/s | ETA: 6985s N2/N1 = 20.073 (413/500) Progress: 165200/200000 (82.6%) | Rate: 5.0 sim/s | ETA: 6957s N2/N1 = 20.447 (414/500) Progress: 165600/200000 (82.8%) | Rate: 5.0 sim/s | ETA: 6930s N2/N1 = 20.828 (415/500) Progress: 166000/200000 (83.0%) | Rate: 4.9 sim/s | ETA: 6901s N2/N1 = 21.216 (416/500) Progress: 166400/200000 (83.2%) | Rate: 4.9 sim/s | ETA: 6872s N2/N1 = 21.611 (417/500) Progress: 166800/200000 (83.4%) | Rate: 4.9 sim/s | ETA: 6843s N2/N1 = 22.013 (418/500) Progress: 167200/200000 (83.6%) | Rate: 4.8 sim/s | ETA: 6813s N2/N1 = 22.423 (419/500) Progress: 167600/200000 (83.8%) | Rate: 4.8 sim/s | ETA: 6798s N2/N1 = 22.841 (420/500) Progress: 168000/200000 (84.0%) | Rate: 4.7 sim/s | ETA: 6766s N2/N1 = 23.267 (421/500) Progress: 168400/200000 (84.2%) | Rate: 4.7 sim/s | ETA: 6734s N2/N1 = 23.700 (422/500) Progress: 168800/200000 (84.4%) | Rate: 4.7 sim/s | ETA: 6700s N2/N1 = 24.142 (423/500) Progress: 169200/200000 (84.6%) | Rate: 4.6 sim/s | ETA: 6664s N2/N1 = 24.591 (424/500) Progress: 169600/200000 (84.8%) | Rate: 4.6 sim/s | ETA: 6628s N2/N1 = 25.049 (425/500) Progress: 170000/200000 (85.0%) | Rate: 4.6 sim/s | ETA: 6592s N2/N1 = 25.516 (426/500) Progress: 170400/200000 (85.2%) | Rate: 4.5 sim/s | ETA: 6553s N2/N1 = 25.991 (427/500) Progress: 170800/200000 (85.4%) | Rate: 4.5 sim/s | ETA: 6514s N2/N1 = 26.476 (428/500) Progress: 171200/200000 (85.6%) | Rate: 4.4 sim/s | ETA: 6476s N2/N1 = 26.969 (429/500) Progress: 171600/200000 (85.8%) | Rate: 4.4 sim/s | ETA: 6436s N2/N1 = 27.471 (430/500) Progress: 172000/200000 (86.0%) | Rate: 4.4 sim/s | ETA: 6395s N2/N1 = 27.983 (431/500) Progress: 172400/200000 (86.2%) | Rate: 4.3 sim/s | ETA: 6352s N2/N1 = 28.504 (432/500) Progress: 172800/200000 (86.4%) | Rate: 4.3 sim/s | ETA: 6309s N2/N1 = 29.035 (433/500) Progress: 173200/200000 (86.6%) | Rate: 4.3 sim/s | ETA: 6265s N2/N1 = 29.576 (434/500) Progress: 173600/200000 (86.8%) | Rate: 4.2 sim/s | ETA: 6219s N2/N1 = 30.127 (435/500) Progress: 174000/200000 (87.0%) | Rate: 4.2 sim/s | ETA: 6172s N2/N1 = 30.688 (436/500) Progress: 174400/200000 (87.2%) | Rate: 4.2 sim/s | ETA: 6124s N2/N1 = 31.260 (437/500) Progress: 174800/200000 (87.4%) | Rate: 4.1 sim/s | ETA: 6075s N2/N1 = 31.842 (438/500) Progress: 175200/200000 (87.6%) | Rate: 4.1 sim/s | ETA: 6093s N2/N1 = 32.436 (439/500) Progress: 175600/200000 (87.8%) | Rate: 4.0 sim/s | ETA: 6042s N2/N1 = 33.040 (440/500) Progress: 176000/200000 (88.0%) | Rate: 4.0 sim/s | ETA: 5989s N2/N1 = 33.655 (441/500) Progress: 176400/200000 (88.2%) | Rate: 4.0 sim/s | ETA: 5940s N2/N1 = 34.282 (442/500) Progress: 176800/200000 (88.4%) | Rate: 3.9 sim/s | ETA: 5884s N2/N1 = 34.921 (443/500) Progress: 177200/200000 (88.6%) | Rate: 3.9 sim/s | ETA: 5827s N2/N1 = 35.572 (444/500) Progress: 177600/200000 (88.8%) | Rate: 3.9 sim/s | ETA: 5768s N2/N1 = 36.234 (445/500) Progress: 178000/200000 (89.0%) | Rate: 3.9 sim/s | ETA: 5710s N2/N1 = 36.909 (446/500) Progress: 178400/200000 (89.2%) | Rate: 3.8 sim/s | ETA: 5650s N2/N1 = 37.597 (447/500) Progress: 178800/200000 (89.4%) | Rate: 3.8 sim/s | ETA: 5589s N2/N1 = 38.297 (448/500) Progress: 179200/200000 (89.6%) | Rate: 3.6 sim/s | ETA: 5831s N2/N1 = 39.011 (449/500) Progress: 179600/200000 (89.8%) | Rate: 3.3 sim/s | ETA: 6146s N2/N1 = 39.737 (450/500) Progress: 180000/200000 (90.0%) | Rate: 3.3 sim/s | ETA: 6065s N2/N1 = 40.478 (451/500) Progress: 180400/200000 (90.2%) | Rate: 3.3 sim/s | ETA: 5983s N2/N1 = 41.232 (452/500) Progress: 180800/200000 (90.4%) | Rate: 3.3 sim/s | ETA: 5899s N2/N1 = 42.000 (453/500) Progress: 181200/200000 (90.6%) | Rate: 3.2 sim/s | ETA: 5814s N2/N1 = 42.782 (454/500) Progress: 181600/200000 (90.8%) | Rate: 3.2 sim/s | ETA: 5727s N2/N1 = 43.579 (455/500) Progress: 182000/200000 (91.0%) | Rate: 3.2 sim/s | ETA: 5638s N2/N1 = 44.391 (456/500) Progress: 182400/200000 (91.2%) | Rate: 3.2 sim/s | ETA: 5548s N2/N1 = 45.218 (457/500) Progress: 182800/200000 (91.4%) | Rate: 3.1 sim/s | ETA: 5549s N2/N1 = 46.060 (458/500) Progress: 183200/200000 (91.6%) | Rate: 3.1 sim/s | ETA: 5454s N2/N1 = 46.918 (459/500) Progress: 183600/200000 (91.8%) | Rate: 3.1 sim/s | ETA: 5359s N2/N1 = 47.792 (460/500) Progress: 184000/200000 (92.0%) | Rate: 3.0 sim/s | ETA: 5261s N2/N1 = 48.683 (461/500) Progress: 184400/200000 (92.2%) | Rate: 3.0 sim/s | ETA: 5163s N2/N1 = 49.590 (462/500) Progress: 184800/200000 (92.4%) | Rate: 3.0 sim/s | ETA: 5063s N2/N1 = 50.513 (463/500) Progress: 185200/200000 (92.6%) | Rate: 3.0 sim/s | ETA: 4961s N2/N1 = 51.454 (464/500) Progress: 185600/200000 (92.8%) | Rate: 3.0 sim/s | ETA: 4858s N2/N1 = 52.413 (465/500) Progress: 186000/200000 (93.0%) | Rate: 2.9 sim/s | ETA: 4753s N2/N1 = 53.389 (466/500) Progress: 186400/200000 (93.2%) | Rate: 2.9 sim/s | ETA: 4647s N2/N1 = 54.384 (467/500) Progress: 186800/200000 (93.4%) | Rate: 2.9 sim/s | ETA: 4540s N2/N1 = 55.397 (468/500) Progress: 187200/200000 (93.6%) | Rate: 2.9 sim/s | ETA: 4430s N2/N1 = 56.429 (469/500) Progress: 187600/200000 (93.8%) | Rate: 2.9 sim/s | ETA: 4320s N2/N1 = 57.480 (470/500) Progress: 188000/200000 (94.0%) | Rate: 2.9 sim/s | ETA: 4208s N2/N1 = 58.551 (471/500) Progress: 188400/200000 (94.2%) | Rate: 2.8 sim/s | ETA: 4095s N2/N1 = 59.642 (472/500) Progress: 188800/200000 (94.4%) | Rate: 2.8 sim/s | ETA: 3980s N2/N1 = 60.753 (473/500) Progress: 189200/200000 (94.6%) | Rate: 2.8 sim/s | ETA: 3863s N2/N1 = 61.885 (474/500) Progress: 189600/200000 (94.8%) | Rate: 2.8 sim/s | ETA: 3745s N2/N1 = 63.038 (475/500) Progress: 190000/200000 (95.0%) | Rate: 2.8 sim/s | ETA: 3625s N2/N1 = 64.212 (476/500) Progress: 190400/200000 (95.2%) | Rate: 2.7 sim/s | ETA: 3504s N2/N1 = 65.408 (477/500) Progress: 190800/200000 (95.4%) | Rate: 2.7 sim/s | ETA: 3380s N2/N1 = 66.627 (478/500) Progress: 191200/200000 (95.6%) | Rate: 2.7 sim/s | ETA: 3255s N2/N1 = 67.868 (479/500) Progress: 191600/200000 (95.8%) | Rate: 2.7 sim/s | ETA: 3128s N2/N1 = 69.132 (480/500) Progress: 192000/200000 (96.0%) | Rate: 2.7 sim/s | ETA: 3001s N2/N1 = 70.420 (481/500) Progress: 192400/200000 (96.2%) | Rate: 2.6 sim/s | ETA: 2871s N2/N1 = 71.732 (482/500) Progress: 192800/200000 (96.4%) | Rate: 2.6 sim/s | ETA: 2739s N2/N1 = 73.068 (483/500) Progress: 193200/200000 (96.6%) | Rate: 2.6 sim/s | ETA: 2605s N2/N1 = 74.429 (484/500) Progress: 193600/200000 (96.8%) | Rate: 2.6 sim/s | ETA: 2469s N2/N1 = 75.816 (485/500) Progress: 194000/200000 (97.0%) | Rate: 2.6 sim/s | ETA: 2331s N2/N1 = 77.228 (486/500) Progress: 194400/200000 (97.2%) | Rate: 2.6 sim/s | ETA: 2191s N2/N1 = 78.667 (487/500) Progress: 194800/200000 (97.4%) | Rate: 2.5 sim/s | ETA: 2049s N2/N1 = 80.132 (488/500) Progress: 195200/200000 (97.6%) | Rate: 2.5 sim/s | ETA: 1905s N2/N1 = 81.625 (489/500) Progress: 195600/200000 (97.8%) | Rate: 2.5 sim/s | ETA: 1758s N2/N1 = 83.146 (490/500) Progress: 196000/200000 (98.0%) | Rate: 2.5 sim/s | ETA: 1609s N2/N1 = 84.695 (491/500) Progress: 196400/200000 (98.2%) | Rate: 2.5 sim/s | ETA: 1459s N2/N1 = 86.272 (492/500) Progress: 196800/200000 (98.4%) | Rate: 2.5 sim/s | ETA: 1306s N2/N1 = 87.880 (493/500) Progress: 197200/200000 (98.6%) | Rate: 2.4 sim/s | ETA: 1151s N2/N1 = 89.517 (494/500) Progress: 197600/200000 (98.8%) | Rate: 2.4 sim/s | ETA: 993ss N2/N1 = 91.184 (495/500) Progress: 198000/200000 (99.0%) | Rate: 2.4 sim/s | ETA: 836s N2/N1 = 92.883 (496/500) Progress: 198400/200000 (99.2%) | Rate: 2.4 sim/s | ETA: 673s N2/N1 = 94.613 (497/500) Progress: 198800/200000 (99.4%) | Rate: 2.4 sim/s | ETA: 509s N2/N1 = 96.376 (498/500) Progress: 199200/200000 (99.6%) | Rate: 2.3 sim/s | ETA: 341s N2/N1 = 98.171 (499/500) Progress: 199600/200000 (99.8%) | Rate: 2.3 sim/s | ETA: 172s N2/N1 = 100.000 (500/500) Progress: 200000/200000 (100.0%) | Rate: 2.3 sim/s | ETA: 0ss Scan completed in 86785.2s (1446.4 min) Average: 0.43s per simulation Results Summary: Valid orbits: 161465 (80.7%) Collisions: 38535 (19.3%) Errors: 0 (0.0%) Period Rel. Std. Dev. Statistics (valid orbits): Min: 0.000000 Max: 1.489600 Mean: 0.290818 Median: 0.257164 ====================================================================== Heat map saved as 'phase_space_heatmap_quantitative.png'
Results saved to 'lowphase_space_heatmap_results.npz'
Figure 22. Image of quantitative heat map. Similar patterns are seen as compared to the discrete phase space diagram. Notably, there is a stark contrast in relative standard deviation between the lower third region and the neon band above it. Collisions increase as charge ratio increases.
Code Block Summary. The above code adapts the previous code for a discrete quantitative phase space scan to a continuous one, with relative standard deviation of an electorn's orbital period as a function of initial velocity angle and charge ratio.

Analysis for Continuous Quantitative Phase Space Scan¶
The above phase space scan was run for 200000 simulations, and is hence much more detailed and informative. We see that the continuous heat map outlines similar patterns compared to the discrete phase-space diagram. However, there are several additional observations we can make using relative standard deviation:
- Collision Patterns: Unlike the discrete phase space, we see that the collision orbits (in red) follow interesting patterns as charge ratio increases. For instance, we see three curves of collisions on the left side of the graph. Furthermore, along the top of the graph, the collisions trace out a distinct pattern that look somewhat like the feathers of a bird. Again, this is something that the previous phase span did not show.
- Neon band above lower third region: We see that there is a neon bandright above the lower third region of stable orbits (purple), indicating a large number of unstable orbits along a curve. The transition between stable to unstable orbits (purple to green to yellow) is surprisingly very sudden, with no indication of a gradual change in relative standard deviation
- Curves of periodicity & quasi-periodicity: When charge ratio is between 1 and 10, we see curves of quasi-periodicity, characterized by distinct colour (such as neon, turquoise, and purple). The curves of quasi-periodicity are especially interesting behaviour because it could never be detected just by a discrete phase space analysis. These curves connect down to the lower regions of the graph and are seen as streaks the follow a counterclockwise path top down. We also see a curve of periodicity in the center of the graph, seens as a purple streak.
For this portion of analysis, we look at these patterns in more detail.
We first create two new functions that allow for a partial phase space scan of our domain: This will allow us to "zoom into" specific regions we wish to study.
# Import useful libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.signal import find_peaks
import math
import time
# Fixed Constants
k = 8.99e9 # Coulomb constant, N·m²/C²
e = -1.6e-19 # Electron charge magnitude, C
n1 = 1.6e-19 # Nucleus 1 charge
me = 9.109e-31 # Electron mass, kg
r0 = 5.29e-11 # Bohr radius, m
# Orbital time
T = 2.5e-15 # time for simulation to run, s
# Nuclei coordinates
distance = 1.2e-10 # vertical distance between two nuclei, m
y1 = distance / 2 # N1 y coordinate, m
x1 = 0 # N1 x coordinate, m
y2 = -distance / 2 # N2 x coordinate, m
x2 = 0 # N2 y coordinate, m
# Initial Velocity
v0 = 2.18e6 # initial velocity (m/s)
# Define your diff_eqns function
def diff_eqns(t, state):
x, y, vx, vy = state
r1x = x - x1
r1y = y - y1
r1 = np.sqrt(r1x**2 + r1y**2)
r2x = x - x2
r2y = y - y2
r2 = np.sqrt(r2x**2 + r2y**2)
# Avoid singularities
r1 = max(r1, 1e-20)
r2 = max(r2, 1e-20)
# Calculate force & acceleration components for N1
fx1 = k * e * n1 * r1x / r1**3
fy1 = k * e * n1 * r1y / r1**3
# Calculate force & acceleration components for N2
fx2 = k * e * n2 * r2x / r2**3
fy2 = k * e * n2 * r2y / r2**3
fx = fx1 + fx2
fy = fy1 + fy2
# Calculate acceleration
accx = fx / me
accy = fy / me
# Return differentials
return vx, vy, accx, accy
# REVISED DETECTION FUNCTIONS
def check_collision(sol, collision_threshold=8.5e-16):
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
collision_n1_indices = np.where(r1 < collision_threshold)[0]
collision_n2_indices = np.where(r2 < collision_threshold)[0]
if len(collision_n1_indices) > 0 or len(collision_n2_indices) > 0:
return True
return False
def calculate_period_std(sol):
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
try:
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
except:
return None
if len(peaks) < 2:
return None
# Calculate periods
periods = np.diff(sol.t[peaks])
if len(periods) < 2:
return None
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
if period_mean == 0:
return None
period_std_rel = period_std / period_mean
return period_std_rel
def analyze_single_orbit_quantitative(angle, n2_value):
try:
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Set n2 as global for diff_eqns to use
global n2
n2 = n2_value
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check for collision first
collided = check_collision(sol)
if collided:
return -1 # Collision marker
# Calculate period variability
rel_std = calculate_period_std(sol)
return rel_std
except Exception as e:
return None # Error
def scan_phase_space_partial(n_theta, n_n2, theta_min, theta_max, n_min, n_max):
print("="*70)
print("PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability")
print("="*70)
# Phase space ranges
n1 = 1.6e-19
theta_range = np.linspace(theta_min, theta_max, n_theta)
n2_n1_ratios = np.linspace(n_min, n_max, n_n2)
n2_range = n1 * n2_n1_ratios
print(f"Theta axis: {n_theta} points from 0° to 90°")
print(f"N2 axis: {n_n2} points from N1/100 to 100*N1")
print(f" N2/N1 range: {n2_n1_ratios[0]:.3f} to {n2_n1_ratios[-1]:.1f}")
print(f"Total simulations: {n_theta * n_n2}")
print("="*70 + "\n")
# Initialize results grid (use NaN for missing data)
results_grid = np.full((n_n2, n_theta), np.nan)
total_sims = n_theta * n_n2
sim_count = 0
start_time = time.time()
# Run simulations
for i, n2_val in enumerate(n2_range):
print(f"\nN2/N1 = {n2_val/n1:.3f} ({i+1}/{n_n2})")
for j, angle_val in enumerate(theta_range):
sim_count += 1
if sim_count % 15 == 0 or sim_count == total_sims:
elapsed = time.time() - start_time
rate = sim_count / elapsed if elapsed > 0 else 0
eta = (total_sims - sim_count) / rate if rate > 0 else 0
print(f" Progress: {sim_count}/{total_sims} ({100*sim_count/total_sims:.1f}%) "
f"| Rate: {rate:.1f} sim/s | ETA: {eta:.0f}s", end='\r')
# Analyze this configuration
rel_std = analyze_single_orbit_quantitative(angle_val, n2_val)
results_grid[i, j] = rel_std if rel_std is not None else np.nan
elapsed = time.time() - start_time
print(f"\n\nScan completed in {elapsed:.1f}s ({elapsed/60:.1f} min)")
print(f"Average: {elapsed/total_sims:.2f}s per simulation\n")
# Print statistics
n_collision = np.sum(results_grid == -1)
n_valid = np.sum(~np.isnan(results_grid) & (results_grid != -1))
n_error = np.sum(np.isnan(results_grid))
valid_data = results_grid[(~np.isnan(results_grid)) & (results_grid != -1)]
print("Results Summary:")
print(f" Valid orbits: {n_valid:4d} ({100*n_valid/total_sims:.1f}%)")
print(f" Collisions: {n_collision:4d} ({100*n_collision/total_sims:.1f}%)")
print(f" Errors: {n_error:4d} ({100*n_error/total_sims:.1f}%)")
if len(valid_data) > 0:
print(f"\nPeriod Rel. Std. Dev. Statistics (valid orbits):")
print(f" Min: {np.min(valid_data):.6f}")
print(f" Max: {np.max(valid_data):.6f}")
print(f" Mean: {np.mean(valid_data):.6f}")
print(f" Median: {np.median(valid_data):.6f}")
print("="*70 + "\n")
return results_grid, theta_range, n2_range
# PLOTTING RESULTS
def plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max,n_min, n_max):
import matplotlib.colors as mcolors
fig = plt.figure(figsize=(10, 8))
# Create meshgrid
theta_grid, n2_grid = np.meshgrid(theta_range, n2_range)
n2_n1_grid = n2_grid / n1
# Separate collision and non-collision data
collision_mask = (results_grid == -1)
data_for_heatmap = results_grid.copy()
data_for_heatmap[collision_mask] = np.nan # Hide collisions from main heatmap
ax = fig.add_subplot(111)
# Plot main heat map (period variability)
im = ax.pcolormesh(theta_grid, n2_n1_grid, data_for_heatmap,
cmap='viridis', shading='auto',
vmin=0, vmax=np.nanpercentile(data_for_heatmap, 95))
# Overlay collision regions in red
collision_data = np.where(collision_mask, 1, np.nan)
ax.pcolormesh(theta_grid, n2_n1_grid, collision_data,
cmap=mcolors.ListedColormap(['#ff0000']),
shading='auto', alpha=0.9)
ax.set_xlabel('Initial Velocity Angle θ (degrees)', fontsize=14, fontweight='bold')
ax.set_ylabel('Charge Ratio N2/N1', fontsize=14, fontweight='bold')
ax.set_title('Phase Space: Orbital Period Variability (Rel. Std. Dev.)\n' +
'θ ∈ [0°, 90°], N2 ∈ [N1/100, 100×N1], V0 = 2.18e6 m/s',
fontsize=16, fontweight='bold', pad=20)
# Use log scale for N2/N1
ax.set_ylim([n_min, n_max])
#ax.set_yticks([0.01, 0.1, 1, 10, 100])
#ax.set_yticklabels(['0.01', '0.1', '1', '10', '100'])
ax.set_xlim([theta_min, theta_max])
# Add grid
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
# Add reference lines
ax.axhline(y=1, color='white', linestyle='--', linewidth=2.5,
alpha=0.8, label='N2 = N1')
ax.axvline(x=0, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
ax.axvline(x=45, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
ax.axvline(x=90, color='white', linestyle=':', linewidth=1.5, alpha=0.5)
# Add colorbar for period variability
cbar = plt.colorbar(im, ax=ax, pad=0.02, aspect=30)
cbar.set_label('Relative Std. Dev. of Period\n(Lower = More Periodic)',
fontsize=12, fontweight='bold')
# Add legend for collision regions
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor='#ff0000', label='Collision'),
Patch(facecolor='#440154', label='Low Variability (Stable)'),
Patch(facecolor='#fde724', label='High Variability (Chaotic)')
]
ax.legend(handles=legend_elements, loc='upper left', fontsize=10,
framealpha=0.9)
plt.tight_layout()
plt.savefig('phase_space_heatmap_quantitative.png', dpi=300, bbox_inches='tight')
print("Heat map saved as 'phase_space_heatmap_quantitative.png'")
plt.show()
return fig
Code Block Summary. These above code adapt the work from the discrete phase space scan to create two new functions, scan_phase_space_partia and plot_phase_space_partial. INstead of taking the $\theta$ and $\frac{N_2}{N_1}$ range as fixed domains, we pass them as parameters in our function, so that we can "Zoom in" and "Zoom out" of regions we want to analyze.
Pattern 1: Periodic Collisions¶

We first analyze the periodic collision patterns observed when the charge ratio is very large. To do this, we first conduct a partial phase space simulation to zoom into the region of study.
import numpy as np
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")
theta_min = 75
theta_max = 90
n_min = 80
n_max = 100
results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=30, n_n2=30, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)
# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)
# Save results
np.savez('partial_phase_space_heatmap_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
====================================================================== ====================================================================== PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability ====================================================================== Theta axis: 30 points from 0° to 90° N2 axis: 30 points from N1/100 to 100*N1 N2/N1 range: 80.000 to 100.0 Total simulations: 900 ====================================================================== N2/N1 = 80.000 (1/30) Progress: 30/900 (3.3%) | Rate: 0.4 sim/s | ETA: 2485s N2/N1 = 80.690 (2/30) Progress: 60/900 (6.7%) | Rate: 0.3 sim/s | ETA: 2469s N2/N1 = 81.379 (3/30) Progress: 90/900 (10.0%) | Rate: 0.3 sim/s | ETA: 2401s N2/N1 = 82.069 (4/30) Progress: 120/900 (13.3%) | Rate: 0.3 sim/s | ETA: 2319s N2/N1 = 82.759 (5/30) Progress: 150/900 (16.7%) | Rate: 0.3 sim/s | ETA: 2257s N2/N1 = 83.448 (6/30) Progress: 180/900 (20.0%) | Rate: 0.3 sim/s | ETA: 2180s N2/N1 = 84.138 (7/30) Progress: 210/900 (23.3%) | Rate: 0.2 sim/s | ETA: 4415s N2/N1 = 84.828 (8/30) Progress: 240/900 (26.7%) | Rate: 0.0 sim/s | ETA: 29370s N2/N1 = 85.517 (9/30) Progress: 270/900 (30.0%) | Rate: 0.0 sim/s | ETA: 25137s N2/N1 = 86.207 (10/30) Progress: 300/900 (33.3%) | Rate: 0.0 sim/s | ETA: 21737s N2/N1 = 86.897 (11/30) Progress: 330/900 (36.7%) | Rate: 0.0 sim/s | ETA: 18936s N2/N1 = 87.586 (12/30) Progress: 360/900 (40.0%) | Rate: 0.0 sim/s | ETA: 16589s N2/N1 = 88.276 (13/30) Progress: 390/900 (43.3%) | Rate: 0.0 sim/s | ETA: 14589s N2/N1 = 88.966 (14/30) Progress: 420/900 (46.7%) | Rate: 0.0 sim/s | ETA: 12860s N2/N1 = 89.655 (15/30) Progress: 450/900 (50.0%) | Rate: 0.0 sim/s | ETA: 11350s N2/N1 = 90.345 (16/30) Progress: 480/900 (53.3%) | Rate: 0.0 sim/s | ETA: 10019s N2/N1 = 91.034 (17/30) Progress: 510/900 (56.7%) | Rate: 0.0 sim/s | ETA: 8833s N2/N1 = 91.724 (18/30) Progress: 540/900 (60.0%) | Rate: 0.0 sim/s | ETA: 7766s N2/N1 = 92.414 (19/30) Progress: 570/900 (63.3%) | Rate: 0.0 sim/s | ETA: 6800s N2/N1 = 93.103 (20/30) Progress: 600/900 (66.7%) | Rate: 0.1 sim/s | ETA: 5922s N2/N1 = 93.793 (21/30) Progress: 630/900 (70.0%) | Rate: 0.1 sim/s | ETA: 5117s N2/N1 = 94.483 (22/30) Progress: 660/900 (73.3%) | Rate: 0.1 sim/s | ETA: 4378s N2/N1 = 95.172 (23/30) Progress: 690/900 (76.7%) | Rate: 0.1 sim/s | ETA: 3694s N2/N1 = 95.862 (24/30) Progress: 720/900 (80.0%) | Rate: 0.1 sim/s | ETA: 3059s N2/N1 = 96.552 (25/30) Progress: 750/900 (83.3%) | Rate: 0.1 sim/s | ETA: 2467s N2/N1 = 97.241 (26/30) Progress: 780/900 (86.7%) | Rate: 0.1 sim/s | ETA: 1913s N2/N1 = 97.931 (27/30) Progress: 810/900 (90.0%) | Rate: 0.1 sim/s | ETA: 1393s N2/N1 = 98.621 (28/30) Progress: 840/900 (93.3%) | Rate: 0.1 sim/s | ETA: 902ss N2/N1 = 99.310 (29/30) Progress: 870/900 (96.7%) | Rate: 0.1 sim/s | ETA: 439s N2/N1 = 100.000 (30/30) Progress: 900/900 (100.0%) | Rate: 0.1 sim/s | ETA: 0ss Scan completed in 12838.0s (214.0 min) Average: 14.26s per simulation Results Summary: Valid orbits: 194 (21.6%) Collisions: 706 (78.4%) Errors: 0 (0.0%) Period Rel. Std. Dev. Statistics (valid orbits): Min: 0.197193 Max: 0.277826 Mean: 0.232681 Median: 0.230353 ====================================================================== Heat map saved as 'phase_space_heatmap_quantitative.png'
Results saved to 'lowphase_space_heatmap_results.npz'
Figure 23. Zoom-in phase space simulation of collision streaks, with domain $75 \leq \theta \leq 90$ and $80 \leq \frac{N_2}{N_1} \leq 100$. Note that the colours in this phase space are different from the original phase scan, since the range in the colour bar for relative standard deviation of period is lower. The $\frac{N_2}{N_1}$ range is displayed as a linear scale.
The periodic curves of collision orbits seen in the original phase space graph are even more pronounced in Figure 23 with the red indicating collision orbits, and green indicating orbits with relative standard deviations of approximately 0.2-0.25. This allows us to pick one combination of parameters that is a collision, and one that is not to sstudy the differences in behaviour more carefully.
Pattern 1: Non-collision Orbit ($N_2 = 82.5 N_1, \theta = 80$)¶
We first start by looking at the orbital trajectory of a non-collision orbit, where we pick $N_2 = 82.5 N_1, \theta = 80$.
# Parameters to vary
n2 = 80 * 1.6e-19 # Nucleus 2 charge
angle = 83 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.1, T, 80)
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.276e-19 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.292e-11 ... -6.759e-12 -6.724e-12]
[ 0.000e+00 2.571e-13 ... -6.898e-11 -6.900e-11]
[ 2.657e+05 -3.321e+03 ... 4.495e+07 4.503e+07]
[ 2.164e+06 1.866e+06 ... -3.276e+07 -3.266e+07]]
sol: None
t_events: None
y_events: None
nfev: 286964
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 24. When $N_2 = 1.3e17 C$ and $\theta = 80$, the electron exhibits a trajectory that does not collide with the nucleus.
From the above, we see that the electorn orbits $N_2$ in an ellptical pattern, but exhibits enough drift to vary its periodic position over time, forming a half-ellptical shape. However, this does not tell us much about the collision aspect of the orbit. To understand this deeper, we generate graphs plotting the distance between the electron, $N_1$ and $N_2$; since the electron is much closer $N_2$, we also include a third subplot that zooms into the the graph when the electron nearly meets $N_2$.
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()
ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend(loc = "upper left")
ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1)
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2")
ax.scatter(sol.t[index], minimum, c = "crimson", s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14)
ax.grid(True)
ax.legend(loc = "upper left")
<matplotlib.legend.Legend at 0x1741ba0a0>
Figure 25a, b, c. Subplots of the distances between the electron and $N_1$ (a) and $N_2$ (b, c). We see sinusoidal-like patterns for all three subplots that are packaged in a larger sinusoidal-like shape.
From the above, we see that both electron distances from $N_1$ and $N_2$ exhibit oscillatory patterns, where the distances various between local minima and maxima; simultaneously, these oscillations are "packaged" in a sinuosoidal-like pattern, with the amplitudes of the oscillatory patterns growing and shrinking periodically as well. This matches the trajectory plot from Figure 24; while the electron orbits in ellptical atterns around $N_2$, its trajectory also grows and shrinks due drift.
Looking at Figure 25c, we knotice that the minimum distance between $N_2$ and the electron is 1.29e-15m. Hence, this is not counted as a collision orbit.
Pattern 1: Collision Orbit ($N_2 = 85N_1, \theta = 80$)¶
Next, we look at a collision orbit in this same region: we pick $N_2 = 85N_1, \theta = 80$.
# Parameters to vary
n2 = 85 * 1.6e-19 # Nucleus 2 charge
angle = 80 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.1, T, 80)
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.317e-19 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.293e-11 ... 7.370e-12 7.716e-12]
[ 0.000e+00 2.612e-13 ... -6.796e-11 -6.792e-11]
[ 3.786e+05 8.387e+04 ... 5.815e+07 5.741e+07]
[ 2.147e+06 1.820e+06 ... 6.556e+06 7.333e+06]]
sol: None
t_events: None
y_events: None
nfev: 298478
njev: 0
nlu: 0
Is this a collision orbit? Yes
Figure 26. When $N_2 = 85N_1, \theta = 80$, the electron exhibits a very similar trajectory as the non-collision orbit. However, the trajectory slightly varies so that the electron collides with $N_2$.
From above, we see that the path the electron takes is very similar to that of the non-collision orbit, except that the electron does collide with $N_2$. Again, we look at the distances between the eelectorn, $N_1$, adn $N_2$ to quantify this.
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()
ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend(loc = "upper left")
ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1)
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2")
ax.scatter(sol.t[index], minimum, c = "crimson", s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14)
ax.grid(True)
ax.legend(loc = "upper left")
<matplotlib.legend.Legend at 0x17444f7f0>
Figure 27 a, b, c. Subplots of the distances between the electron and $N_1$ (a) and $N_2$ (b, c). We see sinusoidal-like patterns for all three subplots that are packaged in a larger sinusoidal-like shape, just like the non-collision orbit. However, the amplitudes of the oscillations are larger.
We see from above that the graphs of Distance from $N_1$ vs. Time and Distance from $N_2$ vs. Time are very similar to that of the non-collision orbit. Howeve,r looking at Figure 27c, we observe that the amplitudes of the oscillations for $N_2$ is larger; the minimum distnace from $N_2$ is 1.65e-16m, yielding a collision with the nucleus.
Hence, from this comparison, we can infer that the periodic patterns seen in the upper half of the full phase space graph is due to the slight variances in amplitudes in the distances between $N_2$ and the electron, but not due to complete differences in orbital trajectory shapes.
Pattern 2: Claw-Mark Collisions¶

Next, we look at the upper left portion of the graph, where collisions in a claw-mark like pattern are seen. Again, we first zoom into this region to see the patterns in more detail.
import numpy as np
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")
theta_min = 0
theta_max = 15
n_min = 5
n_max = 15
results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=30, n_n2=30, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)
# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)
# Save results
np.savez('partial_phase_space_heatmap_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
====================================================================== ====================================================================== PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability ====================================================================== Theta axis: 30 points from 0° to 90° N2 axis: 30 points from N1/100 to 100*N1 N2/N1 range: 5.000 to 15.0 Total simulations: 900 ====================================================================== N2/N1 = 5.000 (1/30) Progress: 30/900 (3.3%) | Rate: 1.7 sim/s | ETA: 501s N2/N1 = 5.345 (2/30) Progress: 60/900 (6.7%) | Rate: 1.7 sim/s | ETA: 496s N2/N1 = 5.690 (3/30) Progress: 90/900 (10.0%) | Rate: 1.6 sim/s | ETA: 494s N2/N1 = 6.034 (4/30) Progress: 120/900 (13.3%) | Rate: 1.6 sim/s | ETA: 486s N2/N1 = 6.379 (5/30) Progress: 150/900 (16.7%) | Rate: 1.6 sim/s | ETA: 477s N2/N1 = 6.724 (6/30) Progress: 180/900 (20.0%) | Rate: 1.5 sim/s | ETA: 469s N2/N1 = 7.069 (7/30) Progress: 210/900 (23.3%) | Rate: 1.5 sim/s | ETA: 460s N2/N1 = 7.414 (8/30) Progress: 240/900 (26.7%) | Rate: 1.5 sim/s | ETA: 448s N2/N1 = 7.759 (9/30) Progress: 270/900 (30.0%) | Rate: 1.4 sim/s | ETA: 436s N2/N1 = 8.103 (10/30) Progress: 300/900 (33.3%) | Rate: 1.4 sim/s | ETA: 424s N2/N1 = 8.448 (11/30) Progress: 330/900 (36.7%) | Rate: 1.4 sim/s | ETA: 409s N2/N1 = 8.793 (12/30) Progress: 360/900 (40.0%) | Rate: 1.4 sim/s | ETA: 394s N2/N1 = 9.138 (13/30) Progress: 390/900 (43.3%) | Rate: 1.3 sim/s | ETA: 378s N2/N1 = 9.483 (14/30) Progress: 420/900 (46.7%) | Rate: 1.3 sim/s | ETA: 361s N2/N1 = 9.828 (15/30) Progress: 450/900 (50.0%) | Rate: 1.3 sim/s | ETA: 344s N2/N1 = 10.172 (16/30) Progress: 480/900 (53.3%) | Rate: 1.3 sim/s | ETA: 326s N2/N1 = 10.517 (17/30) Progress: 510/900 (56.7%) | Rate: 1.3 sim/s | ETA: 307s N2/N1 = 10.862 (18/30) Progress: 540/900 (60.0%) | Rate: 1.3 sim/s | ETA: 288s N2/N1 = 11.207 (19/30) Progress: 570/900 (63.3%) | Rate: 1.2 sim/s | ETA: 267s N2/N1 = 11.552 (20/30) Progress: 600/900 (66.7%) | Rate: 1.2 sim/s | ETA: 246s N2/N1 = 11.897 (21/30) Progress: 630/900 (70.0%) | Rate: 1.2 sim/s | ETA: 225s N2/N1 = 12.241 (22/30) Progress: 660/900 (73.3%) | Rate: 1.2 sim/s | ETA: 202s N2/N1 = 12.586 (23/30) Progress: 690/900 (76.7%) | Rate: 1.2 sim/s | ETA: 179s N2/N1 = 12.931 (24/30) Progress: 720/900 (80.0%) | Rate: 1.2 sim/s | ETA: 156s N2/N1 = 13.276 (25/30) Progress: 750/900 (83.3%) | Rate: 1.1 sim/s | ETA: 131s N2/N1 = 13.621 (26/30) Progress: 780/900 (86.7%) | Rate: 1.1 sim/s | ETA: 106s N2/N1 = 13.966 (27/30) Progress: 810/900 (90.0%) | Rate: 1.1 sim/s | ETA: 81ss N2/N1 = 14.310 (28/30) Progress: 840/900 (93.3%) | Rate: 1.1 sim/s | ETA: 54s N2/N1 = 14.655 (29/30) Progress: 870/900 (96.7%) | Rate: 1.1 sim/s | ETA: 27s N2/N1 = 15.000 (30/30) Progress: 900/900 (100.0%) | Rate: 1.1 sim/s | ETA: 0s Scan completed in 831.3s (13.9 min) Average: 0.92s per simulation Results Summary: Valid orbits: 646 (71.8%) Collisions: 254 (28.2%) Errors: 0 (0.0%) Period Rel. Std. Dev. Statistics (valid orbits): Min: 0.075750 Max: 0.368979 Mean: 0.195883 Median: 0.193745 ====================================================================== Heat map saved as 'phase_space_heatmap_quantitative.png'
Results saved to 'lowphase_space_heatmap_results.npz'
Figure 28. Zoom-in phase space simulation of the region $0 \leq \theta \leq 15$ and $5 \leq \frac{N_2}{N_1} \leq 15$. Streaks of collisions are shown, forming "claw-like" patterns in the phase space graph–this is the "claw mark" we are analyzing.
From above, we see a diagonal streak of collisions from the top right to the bottom left in the center of the graph. We also see collision orbits surrounding it, which, from the oirignal phase space graph, forms the periodic patterns of collisions.
Similar to the analysis from Pattern 1, next we pick 1 parameter combination allow the "claw mark" streak of collisions, and 1 parameter combination that does not yield a collision. We also compare the former to the collisions found in the periodic patterns in Pattern 1 to see if there are any differences.
Pattern 2: Collision Orbit ($N_2 = 18.5 N_1, \theta = 8$)¶
We first look at a collision orbit allow the path of the "claw-mark streak". We pick $N_2 = 10.5 N_1$ and $\theta = 8 $.
# Parameters to vary
n2 = 10.5 * 1.6e-19 # Nucleus 2 charge
angle = 8 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.4, T, 80)
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 4.870e-19 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.392e-11 ... -4.561e-11 -4.594e-11]
[ 0.000e+00 1.147e-13 ... -4.911e-11 -4.756e-11]
[ 2.159e+06 2.013e+06 ... -1.561e+06 -1.298e+06]
[ 3.034e+05 1.682e+05 ... 6.864e+06 6.801e+06]]
sol: None
t_events: None
y_events: None
nfev: 92870
njev: 0
nlu: 0
Is this a collision orbit? Yes
Figure 29. When $N_2 = 10.5 N_1, \theta = 8$, the trajectory made by the electron is similar to that of the periodic collision patterns.
Along the claw-mark, the electorn seems to trace a path similar to the periodic collision orbits. However, the electron exhibits much more drift, creating shape that is much less dense than the previous orbits; even after orbiting for the entire 2.5e-15s, we still see many "holes" in the electron path. We next plot the distances between the electron, $N_1$, and $N_2$.
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()
ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend(loc = "upper left")
ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1)
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2")
ax.scatter(sol.t[index], minimum, c = "crimson", s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14)
ax.grid(True)
ax.legend(loc = "upper left")
<matplotlib.legend.Legend at 0x12728aa90>
Figure 30 a, b, c. Subplots of the distances between the electron and $N_1$ (a) and $N_2$ (b, c). Similar to the previous orbits, we see sinusoidal-like patterns for all three subplots that are packaged in a larger sinusoidal-like shape.
From these subplots, we see that the general graph shows similarities to that of pattern 1: the electrons exhibit oscillatory patterns who amplitudes vary periodically. However, we notice three stark differences:
- Frequency of Oscillations: The average frequency of oscillations is much lower; we can see this from the Figure 30a on the left, where the spacing between each oscillation can be very visibe compared to the distances in the periodic collision patterns, which are so dense that the individual oscillation cannot be seen.
- Frequency of Amplitude Variation: In constrast, the average time it takes for the amplitude to grow and shrink back to its original value is much quicker; due to this, the graphs no longer look like oscillations packaged in a sinusoidal pattern, but in wavepackets.
- Maximum amplitude of distances: From Figure 30 c, the minimum distance between $N_2$ and the electron is nearly an order of magnitude smaller.
Next, we look at a non-collision orbit.
Pattern 2: Non-Collision Orbit ($N_2 = 8N_1$ and $\theta = 4$)¶
For this, we pick $N_2 = 8N_1$ and $\theta = 4$.
# Parameters to vary
n2 = 8 * 1.6e-19 # Nucleus 2 charge
angle = 4 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol = get_trajectory(n2, angle, T)
plot_trajectory(sol, 1.4, T, 80)
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 5.313e-19 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.402e-11 ... 5.471e-11 5.410e-11]
[ 0.000e+00 5.182e-14 ... -8.240e-12 -8.760e-12]
[ 2.175e+06 2.050e+06 ... -2.067e+06 -2.150e+06]
[ 1.521e+05 4.349e+04 ... -1.753e+06 -1.818e+06]]
sol: None
t_events: None
y_events: None
nfev: 75362
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 31. When $N_2 = 8N_1, \theta = 4$, the path traced by the electron is very similar to the collision orbit, but the density of the shape it traces is much denser.
r1 = np.sqrt((sol.y[0] - x1)**2 + (sol.y[1] - y1)**2)
r2 = np.sqrt((sol.y[0] - x2)**2 + (sol.y[1] - y2)**2)
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r1, label = "Distance from N1")
ax[0].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend()
ax[1].plot(sol.t[0:8000],r1[0:8000], label = "Distance from N1")
ax[1].set_title(f"Distance from N1 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N1 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1, 2)
fig.set_size_inches((20,6))
ax[0].plot(sol.t,r2, c = "orange", label = "Distance from N2")
ax[0].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[0].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[0].set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax[0].grid(True)
ax[0].legend(loc = "upper left")
ax[1].plot(sol.t[0:8000],r2[0:8000], c = "orange", label = "Distance from N2")
ax[1].set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax[1].set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax[1].set_xlabel("Time Span Trimmed (s)", fontweight = "bold", fontsize = 14)
ax[1].grid(True)
ax[1].legend(loc = "upper left")
fig, ax = plt.subplots(1)
fig.set_size_inches((20,6))
minimum = min(r2)
index = np.where(r2 == minimum)
ax.plot(sol.t,r2, c = "crimson", label = "Distance from N2")
ax.scatter(sol.t[index], minimum, c = "crimson", s = 100, label = f"Minimum distance = {minimum: 0.2e} m")
ax.set_title(f"Distance from N2 vs. Time", fontweight = "bold", fontsize = 14)
ax.set_ylabel("Distance from N2 (m)", fontweight = "bold", fontsize = 14)
ax.set_xlabel("Time Span Full (s)", fontweight = "bold", fontsize = 14)
ax.set_ylim(0,3e-14)
ax.grid(True)
ax.legend(loc = "upper left")
<matplotlib.legend.Legend at 0x1275d2970>
Figure 32. When $N_2 = 8N_1$ and $\theta = 4$, the distance between the electron, $N_1$, and $N_2$ show very similar graphs, with oscillatory and wavepacket-like behaviour.
From these subplots, we see that the difference described in the collision orbit also applies to the non-collision orbit. Hence, it is not due to these attributes that a clow-mark collision streak is seen. However, the minimum distance between $N_2$ and the electron is an entire two orders of magnitude less than the collision orbit. So again, the collision orbit is simply due to the slight variances in amplitudes in the distances between $N_2$ and the electron, but not due to complete differences in orbital trajectory shapes.
Pattern 3: Neon Band above Lower Third Region¶

Next, we look at the beon band above the lower third region of the phase space analysis. From above, we see that there are very distinct changes in stability, from purple to green to neon yellow; here, we try to understand why this might be the case. Again, we first conduct a partial phase space analysis to zoom into this region.
import numpy as np
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")
theta_min = 0
theta_max = 5
n_min = 0.05
n_max = 0.15
results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=50, n_n2=50, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)
# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)
# Save results
np.savez('partial_phase_space_heatmap_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
====================================================================== ====================================================================== PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability ====================================================================== Theta axis: 50 points from 0° to 90° N2 axis: 50 points from N1/100 to 100*N1 N2/N1 range: 0.050 to 0.1 Total simulations: 2500 ====================================================================== N2/N1 = 0.050 (1/50) Progress: 50/2500 (2.0%) | Rate: 19.3 sim/s | ETA: 127s N2/N1 = 0.052 (2/50) Progress: 100/2500 (4.0%) | Rate: 18.6 sim/s | ETA: 129s N2/N1 = 0.054 (3/50) Progress: 150/2500 (6.0%) | Rate: 18.7 sim/s | ETA: 126s N2/N1 = 0.056 (4/50) Progress: 200/2500 (8.0%) | Rate: 18.1 sim/s | ETA: 127s N2/N1 = 0.058 (5/50) Progress: 250/2500 (10.0%) | Rate: 17.7 sim/s | ETA: 127s N2/N1 = 0.060 (6/50) Progress: 300/2500 (12.0%) | Rate: 17.5 sim/s | ETA: 126s N2/N1 = 0.062 (7/50) Progress: 350/2500 (14.0%) | Rate: 17.4 sim/s | ETA: 124s N2/N1 = 0.064 (8/50) Progress: 400/2500 (16.0%) | Rate: 17.3 sim/s | ETA: 122s N2/N1 = 0.066 (9/50) Progress: 450/2500 (18.0%) | Rate: 17.2 sim/s | ETA: 120s N2/N1 = 0.068 (10/50) Progress: 500/2500 (20.0%) | Rate: 17.0 sim/s | ETA: 118s N2/N1 = 0.070 (11/50) Progress: 550/2500 (22.0%) | Rate: 16.4 sim/s | ETA: 119s N2/N1 = 0.072 (12/50) Progress: 600/2500 (24.0%) | Rate: 16.3 sim/s | ETA: 117s N2/N1 = 0.074 (13/50) Progress: 650/2500 (26.0%) | Rate: 16.2 sim/s | ETA: 114s N2/N1 = 0.077 (14/50) Progress: 700/2500 (28.0%) | Rate: 16.1 sim/s | ETA: 112s N2/N1 = 0.079 (15/50) Progress: 750/2500 (30.0%) | Rate: 16.1 sim/s | ETA: 109s N2/N1 = 0.081 (16/50) Progress: 800/2500 (32.0%) | Rate: 16.0 sim/s | ETA: 106s N2/N1 = 0.083 (17/50) Progress: 850/2500 (34.0%) | Rate: 15.9 sim/s | ETA: 104s N2/N1 = 0.085 (18/50) Progress: 900/2500 (36.0%) | Rate: 15.8 sim/s | ETA: 101s N2/N1 = 0.087 (19/50) Progress: 950/2500 (38.0%) | Rate: 15.3 sim/s | ETA: 102s N2/N1 = 0.089 (20/50) Progress: 1000/2500 (40.0%) | Rate: 15.2 sim/s | ETA: 99s N2/N1 = 0.091 (21/50) Progress: 1050/2500 (42.0%) | Rate: 15.1 sim/s | ETA: 96s N2/N1 = 0.093 (22/50) Progress: 1100/2500 (44.0%) | Rate: 14.9 sim/s | ETA: 94s N2/N1 = 0.095 (23/50) Progress: 1150/2500 (46.0%) | Rate: 14.7 sim/s | ETA: 92s N2/N1 = 0.097 (24/50) Progress: 1200/2500 (48.0%) | Rate: 14.6 sim/s | ETA: 89s N2/N1 = 0.099 (25/50) Progress: 1250/2500 (50.0%) | Rate: 14.5 sim/s | ETA: 86s N2/N1 = 0.101 (26/50) Progress: 1300/2500 (52.0%) | Rate: 14.3 sim/s | ETA: 84s N2/N1 = 0.103 (27/50) Progress: 1350/2500 (54.0%) | Rate: 14.2 sim/s | ETA: 81s N2/N1 = 0.105 (28/50) Progress: 1400/2500 (56.0%) | Rate: 14.1 sim/s | ETA: 78s N2/N1 = 0.107 (29/50) Progress: 1450/2500 (58.0%) | Rate: 14.0 sim/s | ETA: 75s N2/N1 = 0.109 (30/50) Progress: 1500/2500 (60.0%) | Rate: 14.0 sim/s | ETA: 72s N2/N1 = 0.111 (31/50) Progress: 1550/2500 (62.0%) | Rate: 13.9 sim/s | ETA: 68s N2/N1 = 0.113 (32/50) Progress: 1600/2500 (64.0%) | Rate: 13.8 sim/s | ETA: 65s N2/N1 = 0.115 (33/50) Progress: 1650/2500 (66.0%) | Rate: 13.8 sim/s | ETA: 62s N2/N1 = 0.117 (34/50) Progress: 1700/2500 (68.0%) | Rate: 13.7 sim/s | ETA: 58s N2/N1 = 0.119 (35/50) Progress: 1750/2500 (70.0%) | Rate: 13.7 sim/s | ETA: 55s N2/N1 = 0.121 (36/50) Progress: 1800/2500 (72.0%) | Rate: 13.6 sim/s | ETA: 52s N2/N1 = 0.123 (37/50) Progress: 1850/2500 (74.0%) | Rate: 13.5 sim/s | ETA: 48s N2/N1 = 0.126 (38/50) Progress: 1900/2500 (76.0%) | Rate: 13.4 sim/s | ETA: 45s N2/N1 = 0.128 (39/50) Progress: 1950/2500 (78.0%) | Rate: 13.4 sim/s | ETA: 41s N2/N1 = 0.130 (40/50) Progress: 2000/2500 (80.0%) | Rate: 13.3 sim/s | ETA: 38s N2/N1 = 0.132 (41/50) Progress: 2050/2500 (82.0%) | Rate: 13.2 sim/s | ETA: 34s N2/N1 = 0.134 (42/50) Progress: 2100/2500 (84.0%) | Rate: 13.2 sim/s | ETA: 30s N2/N1 = 0.136 (43/50) Progress: 2150/2500 (86.0%) | Rate: 13.1 sim/s | ETA: 27s N2/N1 = 0.138 (44/50) Progress: 2200/2500 (88.0%) | Rate: 13.0 sim/s | ETA: 23s N2/N1 = 0.140 (45/50) Progress: 2250/2500 (90.0%) | Rate: 12.9 sim/s | ETA: 19s N2/N1 = 0.142 (46/50) Progress: 2300/2500 (92.0%) | Rate: 12.9 sim/s | ETA: 16s N2/N1 = 0.144 (47/50) Progress: 2350/2500 (94.0%) | Rate: 12.8 sim/s | ETA: 12s N2/N1 = 0.146 (48/50) Progress: 2400/2500 (96.0%) | Rate: 12.7 sim/s | ETA: 8ss N2/N1 = 0.148 (49/50) Progress: 2450/2500 (98.0%) | Rate: 12.7 sim/s | ETA: 4s N2/N1 = 0.150 (50/50) Progress: 2500/2500 (100.0%) | Rate: 12.7 sim/s | ETA: 0s Scan completed in 197.7s (3.3 min) Average: 0.08s per simulation Results Summary: Valid orbits: 2445 (97.8%) Collisions: 55 (2.2%) Errors: 0 (0.0%) Period Rel. Std. Dev. Statistics (valid orbits): Min: 0.000001 Max: 0.772480 Mean: 0.592599 Median: 0.639730 ====================================================================== Heat map saved as 'phase_space_heatmap_quantitative.png'
Results saved to 'lowphase_space_heatmap_results.npz'
Figure 33. Partial phase space graph of the domain $0 \leq \theta \leq 5$ and $0 \leq \frac{N_2}{N_1} \leq 0.15$. Stark differences in relative standard deviation of the period ranging from 0 to 0.7.
From this partial phase space, we see in further detail that there is very stark differences in relative standard deviation of the period, with horizontal regions of solid colour. Crucially, these regions of distinct colour do not fade into one another, indicating a gradual chnage in stability. Instead, we see very sudden streaks of colour. To understand this more detail, we pick three diffierent parameter combinations to analyze: one with a relative standard deviation of 0-0.1 (purple), one from 0.4-0.6 (green), and one higher than 0.7 (yellow).
Pattern 3: Relative Standard Deviation 0-0.1 ($N_2 = 0.02N_1, \theta = 5$)¶
We first investigate an orbit with a relative standard deviation of under 10%. We pick $N_2 = 0.02N_1, \theta = 5$.
# Parameters to vary
n2 = 0.02 * 1.6e-19 # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.5)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-15e-11, 30e-11)
a.set_xlim(-3e-10,3e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.512e-18 ... 2.500e-14 2.500e-14]
y: [[ 5.290e-11 5.615e-11 ... 8.280e-11 8.128e-11]
[ 0.000e+00 3.196e-13 ... -7.309e-13 -6.788e-13]
[ 2.172e+06 2.132e+06 ... -1.824e+06 -1.840e+06]
[ 1.900e+05 2.322e+05 ... 5.710e+04 6.880e+04]]
sol: None
t_events: None
y_events: None
nfev: 56468
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 34. When $N_2 = 0.02 N_1, \theta = 5$, we see trajectory where the electron only orbits $N_1$ in an off-center elliptical path, with drift causing it to form a shell-like shape.
From these plots, we see that the electron orbits $N_1$ ellptically, with e drift to the left; this creates a shell-like shape, which would eventually form a half-sphere. To understand the relative standard deviation more thoroughly, we look at the histogram of periodicities.
# Set up initial conditions
angle = 5
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 2.5e-15 # time for simulation to run, s
# Set n2 as global for diff_eqns to use
n2 = 0.02 * 1.6e-19
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2e} s\n' \
f'Rel. Std Dev: {period_std_rel:.2%}\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 2 Mean period: 7.36e-16 s Standard deviation: 1.55e-19 s Relative std dev: 0.02% Minimum period: 7.36e-16 s Maximum period: 7.36e-16 s Range: 3.11e-19 s Is the orbit periodic? Yes ==================================================
Figure 35. Histogram of orbital periods when $N_2 = 0.02 N_1, \theta = 5$. All orbital periods lie in two bins, with the relative standard deviation being 0.02%.
From the above, we see that the relative standard deviation is 0.02%. This is compatible with Figure 34, where we see that while the electron experiences drift, its orbital shape generally does not change.
Pattern 3: Relative Standard Deviation 0.4-0.6 ($N_2 = 0.08N_1, \theta = 5$)¶
We first investigate an orbit with a relative standard deviation of under 10%. We pick $N_2 = 0.02N_1, \theta = 5$.
# Parameters to vary
n2 = 0.08 * 1.6e-19 # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.5)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-15e-11, 30e-11)
a.set_xlim(-3e-10,3e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.505e-18 ... 2.500e-14 2.500e-14]
y: [[ 5.290e-11 5.614e-11 ... -2.257e-10 -2.255e-10]
[ 0.000e+00 3.160e-13 ... 1.164e-10 1.182e-10]
[ 2.172e+06 2.130e+06 ... 4.853e+04 6.569e+04]
[ 1.900e+05 2.295e+05 ... 4.908e+05 4.861e+05]]
sol: None
t_events: None
y_events: None
nfev: 57020
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 36. When $N_2 = 0.02 N_1, \theta = 5$. A similar shape is traced, where the electron ellptically orbits $N_1$ but drifts so that its trajectory forms a half-spherical shape.
From above, we see that the electron orbital trajectory is very similar to the first combination of parameters. However, we notice several differences:
- Drift: The electron experiences much more drift with this parameter combination. By the end of the time span, we see that the electron has already traced an entire half-sphere, which is not seen in Figure 34.
- Orbital SHape Variation: As the electron experiences high drift, the eccentricity of the electron's orbits also changes drastically. From the first subplot, we see that the electron's path transforms from a very eccentric elliptical orbit to a much lesser one within 1/8 of its time span. This could possibly affect the variations of orbital periods drastically. We confirm this below with our histograms of orbital periods.
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 2.5e-15 # time for simulation to run, s
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2e} s\n' \
f'Rel. Std Dev: {period_std_rel:.2%}\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
print("="*50)
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 4 Mean period: 4.31e-16 s Standard deviation: 2.31e-16 s Relative std dev: 53.50% Minimum period: 3.22e-17 s Maximum period: 5.74e-16 s Range: 5.42e-16 s Is the orbit periodic? No ==================================================
Figure 37. Histogram of orbital periods when $N_2 = 0.08 N_1, \theta = 5$. All orbital periods lie in two bins, with the relative standard deviation being 53.5%.
As seen from the histogram, the relative standard devaition has skyrocketed to 53.5%, most likely due both to the electron drift and its change in eccentricity in orbits. This explains why there is a stark difference in relative standard deviation in the original phase space graph. We expect the difference to be even more pronounced when charge ratio is slightly higher; we check this in our third iteration.
Pattern 3: Relative Standard Deviation > 0.7 ($N_2 = 0.115N_1, \theta = 5$)¶
We first investigate an orbit with a relative standard deviation of under 10%. We pick $N_2 = 0.02N_1, \theta = 5$.
# Parameters to vary
n2 = 0.115 * 1.6e-19 # Nucleus 2 charge
angle = 5 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.5)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-15e-11, 30e-11)
a.set_xlim(-3e-10,3e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.504e-18 ... 2.500e-14 2.500e-14]
y: [[ 5.290e-11 5.613e-11 ... 2.208e-11 1.918e-11]
[ 0.000e+00 3.147e-13 ... 1.741e-12 1.195e-12]
[ 2.172e+06 2.128e+06 ... -2.539e+06 -2.566e+06]
[ 1.900e+05 2.280e+05 ... -5.106e+05 -4.476e+05]]
sol: None
t_events: None
y_events: None
nfev: 65420
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 38. When $N_2 = 0.02 N_1, \theta = 5$, the same half=spherical shape is traced, with an even larger electron drift and change in eccentric orbit.
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 2.5e-15 # time for simulation to run, s
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2e} s\n' \
f'Rel. Std Dev: {period_std_rel:.2%}\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 5 Mean period: 3.07e-16 s Standard deviation: 2.25e-16 s Relative std dev: 73.30% Minimum period: 3.16e-17 s Maximum period: 5.08e-16 s Range: 4.77e-16 s Is the orbit periodic? No
Figure 39. Histogram of orbital periods when $N_2 = 0.115 N_1, \theta = 5$. All orbital periods lie in two bins, with the relative standard deviation being 73.3%.
With the relative standard deviation being 73.3%, we see that this is indeed the case.
Hence, the neon bands above the third region occur most likely due to an increase in electron drift, as well as increased change in orbital eccentricities.
Pattern 4: Curves of Periodicity¶

Finally, we look at the curve of periodicity above. We first conduct a partial phase space scan of the region.
import numpy as np
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")
theta_min = 32
theta_max = 37
n_min = 0.5
n_max = 1
results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=50, n_n2=50, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)
# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)
# Save results
np.savez('partial_phase_space_heatmap_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
====================================================================== ====================================================================== PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability ====================================================================== Theta axis: 50 points from 0° to 90° N2 axis: 50 points from N1/100 to 100*N1 N2/N1 range: 0.500 to 1.0 Total simulations: 2500 ====================================================================== N2/N1 = 0.500 (1/50) Progress: 45/2500 (1.8%) | Rate: 7.1 sim/s | ETA: 345s N2/N1 = 0.510 (2/50) Progress: 90/2500 (3.6%) | Rate: 7.1 sim/s | ETA: 341s N2/N1 = 0.520 (3/50) Progress: 150/2500 (6.0%) | Rate: 7.2 sim/s | ETA: 327s N2/N1 = 0.531 (4/50) Progress: 195/2500 (7.8%) | Rate: 7.1 sim/s | ETA: 325s N2/N1 = 0.541 (5/50) Progress: 240/2500 (9.6%) | Rate: 7.0 sim/s | ETA: 322s N2/N1 = 0.551 (6/50) Progress: 300/2500 (12.0%) | Rate: 7.1 sim/s | ETA: 312s N2/N1 = 0.561 (7/50) Progress: 345/2500 (13.8%) | Rate: 7.0 sim/s | ETA: 307s N2/N1 = 0.571 (8/50) Progress: 390/2500 (15.6%) | Rate: 7.0 sim/s | ETA: 302s N2/N1 = 0.582 (9/50) Progress: 450/2500 (18.0%) | Rate: 7.0 sim/s | ETA: 294s N2/N1 = 0.592 (10/50) Progress: 495/2500 (19.8%) | Rate: 6.9 sim/s | ETA: 290s N2/N1 = 0.602 (11/50) Progress: 540/2500 (21.6%) | Rate: 6.9 sim/s | ETA: 286s N2/N1 = 0.612 (12/50) Progress: 600/2500 (24.0%) | Rate: 6.8 sim/s | ETA: 279s N2/N1 = 0.622 (13/50) Progress: 645/2500 (25.8%) | Rate: 6.8 sim/s | ETA: 273s N2/N1 = 0.633 (14/50) Progress: 690/2500 (27.6%) | Rate: 6.7 sim/s | ETA: 269s N2/N1 = 0.643 (15/50) Progress: 750/2500 (30.0%) | Rate: 6.7 sim/s | ETA: 262s N2/N1 = 0.653 (16/50) Progress: 795/2500 (31.8%) | Rate: 6.6 sim/s | ETA: 257s N2/N1 = 0.663 (17/50) Progress: 840/2500 (33.6%) | Rate: 6.6 sim/s | ETA: 252s N2/N1 = 0.673 (18/50) Progress: 900/2500 (36.0%) | Rate: 6.5 sim/s | ETA: 245s N2/N1 = 0.684 (19/50) Progress: 945/2500 (37.8%) | Rate: 6.5 sim/s | ETA: 239s N2/N1 = 0.694 (20/50) Progress: 990/2500 (39.6%) | Rate: 6.5 sim/s | ETA: 234s N2/N1 = 0.704 (21/50) Progress: 1050/2500 (42.0%) | Rate: 6.4 sim/s | ETA: 226s N2/N1 = 0.714 (22/50) Progress: 1095/2500 (43.8%) | Rate: 6.4 sim/s | ETA: 220s N2/N1 = 0.724 (23/50) Progress: 1140/2500 (45.6%) | Rate: 6.3 sim/s | ETA: 215s N2/N1 = 0.735 (24/50) Progress: 1200/2500 (48.0%) | Rate: 6.3 sim/s | ETA: 207s N2/N1 = 0.745 (25/50) Progress: 1245/2500 (49.8%) | Rate: 6.2 sim/s | ETA: 201s N2/N1 = 0.755 (26/50) Progress: 1290/2500 (51.6%) | Rate: 6.2 sim/s | ETA: 195s N2/N1 = 0.765 (27/50) Progress: 1350/2500 (54.0%) | Rate: 6.2 sim/s | ETA: 186s N2/N1 = 0.776 (28/50) Progress: 1395/2500 (55.8%) | Rate: 6.1 sim/s | ETA: 180s N2/N1 = 0.786 (29/50) Progress: 1440/2500 (57.6%) | Rate: 6.1 sim/s | ETA: 174s N2/N1 = 0.796 (30/50) Progress: 1500/2500 (60.0%) | Rate: 6.0 sim/s | ETA: 165s N2/N1 = 0.806 (31/50) Progress: 1545/2500 (61.8%) | Rate: 6.0 sim/s | ETA: 159s N2/N1 = 0.816 (32/50) Progress: 1590/2500 (63.6%) | Rate: 6.0 sim/s | ETA: 153s N2/N1 = 0.827 (33/50) Progress: 1650/2500 (66.0%) | Rate: 5.9 sim/s | ETA: 144s N2/N1 = 0.837 (34/50) Progress: 1695/2500 (67.8%) | Rate: 5.9 sim/s | ETA: 137s N2/N1 = 0.847 (35/50) Progress: 1740/2500 (69.6%) | Rate: 5.9 sim/s | ETA: 130s N2/N1 = 0.857 (36/50) Progress: 1800/2500 (72.0%) | Rate: 5.8 sim/s | ETA: 120s N2/N1 = 0.867 (37/50) Progress: 1845/2500 (73.8%) | Rate: 5.8 sim/s | ETA: 113s N2/N1 = 0.878 (38/50) Progress: 1890/2500 (75.6%) | Rate: 5.8 sim/s | ETA: 106s N2/N1 = 0.888 (39/50) Progress: 1950/2500 (78.0%) | Rate: 5.7 sim/s | ETA: 96ss N2/N1 = 0.898 (40/50) Progress: 1995/2500 (79.8%) | Rate: 5.7 sim/s | ETA: 89s N2/N1 = 0.908 (41/50) Progress: 2040/2500 (81.6%) | Rate: 5.7 sim/s | ETA: 81s N2/N1 = 0.918 (42/50) Progress: 2100/2500 (84.0%) | Rate: 5.6 sim/s | ETA: 71s N2/N1 = 0.929 (43/50) Progress: 2145/2500 (85.8%) | Rate: 5.6 sim/s | ETA: 64s N2/N1 = 0.939 (44/50) Progress: 2190/2500 (87.6%) | Rate: 5.5 sim/s | ETA: 56s N2/N1 = 0.949 (45/50) Progress: 2250/2500 (90.0%) | Rate: 5.5 sim/s | ETA: 45s N2/N1 = 0.959 (46/50) Progress: 2295/2500 (91.8%) | Rate: 5.5 sim/s | ETA: 37s N2/N1 = 0.969 (47/50) Progress: 2340/2500 (93.6%) | Rate: 5.4 sim/s | ETA: 30s N2/N1 = 0.980 (48/50) Progress: 2400/2500 (96.0%) | Rate: 5.4 sim/s | ETA: 19s N2/N1 = 0.990 (49/50) Progress: 2445/2500 (97.8%) | Rate: 5.4 sim/s | ETA: 10s N2/N1 = 1.000 (50/50) Progress: 2500/2500 (100.0%) | Rate: 5.3 sim/s | ETA: 0s Scan completed in 468.6s (7.8 min) Average: 0.19s per simulation Results Summary: Valid orbits: 2377 (95.1%) Collisions: 123 (4.9%) Errors: 0 (0.0%) Period Rel. Std. Dev. Statistics (valid orbits): Min: 0.000902 Max: 0.612470 Mean: 0.218956 Median: 0.314808 ====================================================================== Heat map saved as 'phase_space_heatmap_quantitative.png'
Results saved to 'lowphase_space_heatmap_results.npz'
Figure 40. Partial phase space graph when $32 \leq \theta \leq 37, 0.5 \leq \frac{N_2}{N_1} \leq 1$. Curve of periodicity seen surrounded by quasi-periodic, unstable orbits.
We pick one stable orbit and one unstable orbit to analyze their differences.
Pattern 4: Stable Orbit ($N_2 = 0.8N_1, \theta = 36$)¶
# Parameters to vary
n2 = 0.7 * 1.6e-19 # Nucleus 2 charge
angle = 35 # Launch angle in degrees
T = 2.5e-14 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.4)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-15e-11, 15e-11)
a.set_xlim(-1.25e-10,1.25e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 2.010e-18 ... 2.500e-14 2.500e-14]
y: [[ 5.290e-11 5.640e-11 ... -6.273e-11 -6.283e-11]
[ 0.000e+00 2.531e-12 ... -9.004e-11 -8.944e-11]
[ 1.786e+06 1.697e+06 ... -2.440e+05 -2.287e+05]
[ 1.250e+06 1.269e+06 ... 1.465e+06 1.475e+06]]
sol: None
t_events: None
y_events: None
nfev: 170804
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 41. When $N_2 = 0.7N_1, \theta = 35$, the electron follows a figure-eight pattern, with its drift forming an ellipsoid-like shape.
We now compare this to an unstable orbit.
Pattern 4: Unstable Orbit ($N_2 = 0.6N_1, \theta = 32$)¶
# Parameters to vary
n2 = 0.6 * 1.6e-19 # Nucleus 2 charge
angle = 32 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.45)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-15e-11, 15e-11)
a.set_xlim(-1.5e-10,1.5e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 2.156e-18 ... 2.499e-15 2.500e-15]
y: [[ 5.290e-11 5.679e-11 ... -8.746e-11 -8.848e-11]
[ 0.000e+00 2.518e-12 ... 3.847e-11 4.047e-11]
[ 1.849e+06 1.759e+06 ... -7.486e+05 -6.979e+05]
[ 1.155e+06 1.181e+06 ... 1.413e+06 1.414e+06]]
sol: None
t_events: None
y_events: None
nfev: 15374
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 42. When $N_2 = 0.6N_1, \theta = 32$, the electron follows a completely different pattern, but still ultimately traces an ellipsoid-like shape. Time span is reduced to see orbital path more clearly.
Comparing this to Figure 41, we see that although the electron's path ultimately still creates an ellipsoid-like shape, its orbital path is drastically different. On one hand, in the stable orbit, the electron follows a figure-eight like pattern, with slight drift, creating an ellipsoid; crucially, because the drift, we see that the orbital shapes are roughly the same, contributing to a low relative standard deviation. On the other hand, in the unstable orbit, the electron seems to circle around $N_1$ and $N_2$ in a on on the right side, with its orbital shape growing rapidly to produce an ellipsoid. As such, it is reasonable for hte orbital period relative standard deviation be much larger.
Hence, we attribute this pattern to the rate of drift change the electron undergoes.
Part IV: Analyzing Numerical Artifacts and Quantification of Stability¶

In our final section, we analyze some of the numeritcal artifacts that are seen in our continuous phase space scan. We specifically look at the above region, where we see unexpected shifts in color that do not align with the curves seen in the broader graph. We first conduct a partial phase space scan of the region.
import numpy as np
print("\n" + "="*70)
input("Press Enter to start quantitative phase space scan...")
theta_min = 70
theta_max = 90
n_min = 0.05
n_max = 0.1
results_grid, theta_range, n2_range = scan_phase_space_partial(n_theta=50, n_n2=50, theta_min = theta_min, theta_max = theta_max, n_min = n_min, n_max = n_max)
# Plot results
plot_phase_space_partial(results_grid, theta_range, n2_range, theta_min, theta_max, n_min, n_max)
# Save results
np.savez('partial_phase_space_heatmap_results.npz',
results_grid=results_grid,
theta_range=theta_range,
n2_range=n2_range,
n1=n1)
print("\nResults saved to 'lowphase_space_heatmap_results.npz'")
====================================================================== ====================================================================== PARTIAL 2D QUANTITATIVE PHASE SPACE SCAN: Period Variability ====================================================================== Theta axis: 50 points from 0° to 90° N2 axis: 50 points from N1/100 to 100*N1 N2/N1 range: 0.050 to 0.1 Total simulations: 2500 ====================================================================== N2/N1 = 0.050 (1/50) Progress: 45/2500 (1.8%) | Rate: 18.2 sim/s | ETA: 135s N2/N1 = 0.051 (2/50) Progress: 90/2500 (3.6%) | Rate: 18.1 sim/s | ETA: 133s N2/N1 = 0.052 (3/50) Progress: 150/2500 (6.0%) | Rate: 18.0 sim/s | ETA: 130s N2/N1 = 0.053 (4/50) Progress: 195/2500 (7.8%) | Rate: 17.9 sim/s | ETA: 128s N2/N1 = 0.054 (5/50) Progress: 240/2500 (9.6%) | Rate: 17.9 sim/s | ETA: 126s N2/N1 = 0.055 (6/50) Progress: 300/2500 (12.0%) | Rate: 17.6 sim/s | ETA: 125s N2/N1 = 0.056 (7/50) Progress: 345/2500 (13.8%) | Rate: 16.8 sim/s | ETA: 129s N2/N1 = 0.057 (8/50) Progress: 390/2500 (15.6%) | Rate: 16.7 sim/s | ETA: 127s N2/N1 = 0.058 (9/50) Progress: 450/2500 (18.0%) | Rate: 16.3 sim/s | ETA: 125s N2/N1 = 0.059 (10/50) Progress: 495/2500 (19.8%) | Rate: 16.1 sim/s | ETA: 124s N2/N1 = 0.060 (11/50) Progress: 540/2500 (21.6%) | Rate: 16.0 sim/s | ETA: 122s N2/N1 = 0.061 (12/50) Progress: 600/2500 (24.0%) | Rate: 15.9 sim/s | ETA: 119s N2/N1 = 0.062 (13/50) Progress: 645/2500 (25.8%) | Rate: 15.9 sim/s | ETA: 117s N2/N1 = 0.063 (14/50) Progress: 690/2500 (27.6%) | Rate: 15.8 sim/s | ETA: 115s N2/N1 = 0.064 (15/50) Progress: 750/2500 (30.0%) | Rate: 15.5 sim/s | ETA: 113s N2/N1 = 0.065 (16/50) Progress: 795/2500 (31.8%) | Rate: 15.3 sim/s | ETA: 111s N2/N1 = 0.066 (17/50) Progress: 840/2500 (33.6%) | Rate: 15.3 sim/s | ETA: 108s N2/N1 = 0.067 (18/50) Progress: 900/2500 (36.0%) | Rate: 15.3 sim/s | ETA: 105s N2/N1 = 0.068 (19/50) Progress: 945/2500 (37.8%) | Rate: 15.2 sim/s | ETA: 102s N2/N1 = 0.069 (20/50) Progress: 990/2500 (39.6%) | Rate: 15.2 sim/s | ETA: 99ss N2/N1 = 0.070 (21/50) Progress: 1050/2500 (42.0%) | Rate: 15.3 sim/s | ETA: 95s N2/N1 = 0.071 (22/50) Progress: 1095/2500 (43.8%) | Rate: 15.3 sim/s | ETA: 92s N2/N1 = 0.072 (23/50) Progress: 1140/2500 (45.6%) | Rate: 15.3 sim/s | ETA: 89s N2/N1 = 0.073 (24/50) Progress: 1200/2500 (48.0%) | Rate: 15.3 sim/s | ETA: 85s N2/N1 = 0.074 (25/50) Progress: 1245/2500 (49.8%) | Rate: 15.3 sim/s | ETA: 82s N2/N1 = 0.076 (26/50) Progress: 1290/2500 (51.6%) | Rate: 15.3 sim/s | ETA: 79s N2/N1 = 0.077 (27/50) Progress: 1350/2500 (54.0%) | Rate: 15.3 sim/s | ETA: 75s N2/N1 = 0.078 (28/50) Progress: 1395/2500 (55.8%) | Rate: 15.3 sim/s | ETA: 72s N2/N1 = 0.079 (29/50) Progress: 1440/2500 (57.6%) | Rate: 15.3 sim/s | ETA: 69s N2/N1 = 0.080 (30/50) Progress: 1500/2500 (60.0%) | Rate: 15.3 sim/s | ETA: 65s N2/N1 = 0.081 (31/50) Progress: 1545/2500 (61.8%) | Rate: 15.3 sim/s | ETA: 62s N2/N1 = 0.082 (32/50) Progress: 1590/2500 (63.6%) | Rate: 15.3 sim/s | ETA: 59s N2/N1 = 0.083 (33/50) Progress: 1650/2500 (66.0%) | Rate: 15.3 sim/s | ETA: 55s N2/N1 = 0.084 (34/50) Progress: 1695/2500 (67.8%) | Rate: 15.3 sim/s | ETA: 53s N2/N1 = 0.085 (35/50) Progress: 1740/2500 (69.6%) | Rate: 15.3 sim/s | ETA: 50s N2/N1 = 0.086 (36/50) Progress: 1800/2500 (72.0%) | Rate: 15.3 sim/s | ETA: 46s N2/N1 = 0.087 (37/50) Progress: 1845/2500 (73.8%) | Rate: 15.3 sim/s | ETA: 43s N2/N1 = 0.088 (38/50) Progress: 1890/2500 (75.6%) | Rate: 15.3 sim/s | ETA: 40s N2/N1 = 0.089 (39/50) Progress: 1950/2500 (78.0%) | Rate: 15.3 sim/s | ETA: 36s N2/N1 = 0.090 (40/50) Progress: 1995/2500 (79.8%) | Rate: 15.3 sim/s | ETA: 33s N2/N1 = 0.091 (41/50) Progress: 2040/2500 (81.6%) | Rate: 15.3 sim/s | ETA: 30s N2/N1 = 0.092 (42/50) Progress: 2100/2500 (84.0%) | Rate: 15.3 sim/s | ETA: 26s N2/N1 = 0.093 (43/50) Progress: 2145/2500 (85.8%) | Rate: 15.3 sim/s | ETA: 23s N2/N1 = 0.094 (44/50) Progress: 2190/2500 (87.6%) | Rate: 15.3 sim/s | ETA: 20s N2/N1 = 0.095 (45/50) Progress: 2250/2500 (90.0%) | Rate: 15.3 sim/s | ETA: 16s N2/N1 = 0.096 (46/50) Progress: 2295/2500 (91.8%) | Rate: 15.2 sim/s | ETA: 13s N2/N1 = 0.097 (47/50) Progress: 2340/2500 (93.6%) | Rate: 15.2 sim/s | ETA: 11s N2/N1 = 0.098 (48/50) Progress: 2400/2500 (96.0%) | Rate: 15.2 sim/s | ETA: 7ss N2/N1 = 0.099 (49/50) Progress: 2445/2500 (97.8%) | Rate: 15.2 sim/s | ETA: 4s N2/N1 = 0.100 (50/50) Progress: 2500/2500 (100.0%) | Rate: 15.2 sim/s | ETA: 0s Scan completed in 164.4s (2.7 min) Average: 0.07s per simulation Results Summary: Valid orbits: 2500 (100.0%) Collisions: 0 (0.0%) Errors: 0 (0.0%) Period Rel. Std. Dev. Statistics (valid orbits): Min: 0.000016 Max: 0.888356 Mean: 0.541145 Median: 0.634151 ====================================================================== Heat map saved as 'phase_space_heatmap_quantitative.png'
Results saved to 'lowphase_space_heatmap_results.npz'
Figure 43. Partial phase space simulation with the domain $70 \leq \theta \leq 90$, $0.05 \leq \frac{N_2}{N_1} \leq 0.1$. Possible numerical artifacts or shown.

From above, we see a partial phase space with possible numerical artifacts, characterized by sudden changes of colour. For instance, ther is an abrupt change in colour from pruple to green in the bottom of the graph. These results are different than the ones analyzed in Part III, since they do not follow any predictablw curve or pattern. We look more into the subregion above, picking one parameter combination in the green portion and one in the purple.
Green Portion: $N_2 = 0.065N_1, \theta = 82.5$¶
We first find a parameter combination in the green portion of the subregion. We pick $N_2 = 0.065N_1, \theta = 82.5$.
# Parameters to vary
n2 = 0.065 * 1.6e-19 # Nucleus 2 charge
angle = 82.5 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.35)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-25e-11, 25e-11)
a.set_xlim(-2e-10,2e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.398e-18 ... 2.498e-15 2.500e-15]
y: [[ 5.290e-11 5.327e-11 ... -2.582e-11 -2.428e-11]
[ 0.000e+00 3.049e-12 ... -9.210e-11 -9.438e-11]
[ 2.845e+05 2.442e+05 ... 7.957e+05 8.099e+05]
[ 2.161e+06 2.201e+06 ... -1.210e+06 -1.176e+06]]
sol: None
t_events: None
y_events: None
nfev: 5672
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 44. When $N_2 = 0.065 N_1, \theta = 82.5$ the electron follows elliptical paths orbiting around both $N_1$ and $N_2$, with drift shfting the ellptical orbits counterclockwise.
We also include a histogram of the orbital periods.
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 2.5e-15 # time for simulation to run, s
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2e} s\n' \
f'Rel. Std Dev: {period_std_rel:.2%}\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 4 Mean period: 4.61e-16 s Standard deviation: 2.25e-16 s Relative std dev: 48.81% Minimum period: 7.14e-17 s Maximum period: 5.95e-16 s Range: 5.24e-16 s Is the orbit periodic? No
Figure 45. Histogram of orbital periods when $N_2 = 0.065 N_1, \theta = 82.5$. All orbital periods lie in two bins, with the relative standard deviation being 48.81%.
We do the exact same thing with a combination of parameters in the purple region.
Purple Portion: $N_2 = 0.05N_1, \theta = 82.5$¶
# Parameters to vary
n2 = 0.05 * 1.6e-19 # Nucleus 2 charge
angle = 82.5 # Launch angle in degrees
T = 2.5e-15 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
#Plot trajectory
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 12)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=1.35)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
#x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
#y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
for a in ax:
a.set_ylim(-25e-11, 25e-11)
a.set_xlim(-2e-10,2e-10)
plt.show()
# Set up initial conditions
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
# Run simulation
t_span = (0, T)
# Check Periodicity
is_periodic = check_periodicity(sol)
is_collision = check_collision(sol)
print(f"Is this a collision orbit? {'Yes' if is_collision else 'No'}")
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 1.396e-18 ... 2.500e-15 2.500e-15]
y: [[ 5.290e-11 5.327e-11 ... 8.923e-11 8.923e-11]
[ 0.000e+00 3.046e-12 ... -4.898e-11 -4.894e-11]
[ 2.845e+05 2.448e+05 ... -2.004e+05 -2.007e+05]
[ 2.161e+06 2.201e+06 ... 1.396e+06 1.396e+06]]
sol: None
t_events: None
y_events: None
nfev: 4856
njev: 0
nlu: 0
Is this a collision orbit? No
Figure 46. A similar trajectory can be seen in the purple region when $N_2 = 0.05N_1, \theta = 82.5$ .
theta = math.radians(angle)
vx0 = math.cos(theta) * v0
vy0 = math.sin(theta) * v0
state0 = (r0, 0, vx0, vy0)
T = 2.5e-15 # time for simulation to run, s
# Run simulation
t_span = (0, T)
sol = solve_ivp(diff_eqns, t_span, state0,
rtol=1e-9, atol=1e-9,
max_step=1e-17)
# Check Periodicity
is_periodic = check_periodicity(sol)
# Generate Graph of Periods
# Calculate distance from starting position
x0, y0 = sol.y[0][0], sol.y[1][0]
distances = np.sqrt((sol.y[0] - x0)**2 + (sol.y[1] - y0)**2)
# Find local minima (returns to starting region)
peaks, _ = find_peaks(-distances, distance=len(distances)//20)
# Calculate periods
periods = np.diff(sol.t[peaks])
# Calculate relative standard deviation
period_mean = np.mean(periods)
period_std = np.std(periods)
period_std_rel = period_std / period_mean
n = []
for i in range(len(periods)):
n.append(i+1)
# Make histogram of periods
figsize = (10, 7)
plt.figure(figsize=figsize)
# Create histogram
counts, bins, patches = plt.hist(periods, bins=10, color="#7cc4d6",
rwidth=0.85, label='Orbital Periods',
edgecolor='black', alpha=0.7)
# Graph vertical line at mean period
plt.axvline(x=period_mean, color='red', linestyle='--', linewidth=2,
label=f'Mean Period = {period_mean:.2e} s')
stats_text = f'Statistics:\n' \
f'Mean: {period_mean:.2e} s\n' \
f'Std Dev: {period_std:.2g} s\n' \
f'Rel. Std Dev: {period_std_rel * 100:.2g} %\n' \
f'Min: {np.min(periods):.2e} s\n' \
f'Max: {np.max(periods):.2e} s\n' \
f'N orbits: {len(periods)}'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
plt.text(0.98, 0.97, stats_text, transform=plt.gca().transAxes,
fontsize=10, verticalalignment='top', horizontalalignment='right',
bbox=props, family='monospace')
# Grid for easier reading
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
plt.axvspan(period_mean - period_std, period_mean + period_std,
alpha=0.2, color='orange', label='±1σ range')
# Legend
plt.legend(loc='upper left', fontsize=10, framealpha=0.9)
plt.title('Histogram of Orbital Periods', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Orbital Period (s)', fontsize=14)
plt.ylabel('Frequency', fontsize=14)
print("\n" + "="*50)
print("ORBITAL PERIOD STATISTICS")
print("="*50)
print(f"Number of complete orbits: {len(periods)}")
print(f"Mean period: {period_mean:.2e} s")
print(f"Standard deviation: {period_std:.2e} s")
print(f"Relative std dev: {period_std_rel:.2%}")
print(f"Minimum period: {np.min(periods):.2e} s")
print(f"Maximum period: {np.max(periods):.2e} s")
print(f"Range: {np.max(periods) - np.min(periods):.2e} s \n")
print(f"Is the orbit periodic? {'Yes' if is_periodic else 'No'}")
================================================== ORBITAL PERIOD STATISTICS ================================================== Number of complete orbits: 2 Mean period: 6.34e-16 s Standard deviation: 1.12e-20 s Relative std dev: 0.00% Minimum period: 6.34e-16 s Maximum period: 6.34e-16 s Range: 2.24e-20 s Is the orbit periodic? Yes
Figure 47. Histogram of orbital periods when $N_2 = 0.05N_1, \theta = 82.5$. All orbital periods lie in two bins, with the relative standard deviation being 0.0018%.
Comparing the green and purple regions, we see that the green region has a stnadard deviation of nearly 50%, while the purple region has eseentially no standard deviation. In Figure 45, we see that the reason the standard deviation is so high is due to one occurrence of an orbital period being close to 1e-16 seconds. This is more than five times less than the other periods. Why might this be? Looking at the orbital paths in the green region (Figure 44), a possible explanation for this is the last orbit the electron makes around $N_1$ and $N_2$ in the time span. In the last orbit, the electron first travels near $N_1$, yielding a local minima; it then travels past $N_1$, turns around, and travels downwards towards $N_2$. Because of the unique eccentricity of the electron's elliptical path, another local minima is measured as the electron switches direction approaches $N_2$. As a result, a very brief period is detected by the function that quantifies stability, even though it should not. This is most likely why the standard deviation is so large in the green region. Since the green region is due to a detection of an inaccurate period, we can classify this occurrence in our phase space as a numerical artifact.
More generally, this analysis shows that there are limitations in this project's quantification of stability. Specifically, there are occurrences where orbital periods are detected that do not exist and vice versa. This will affect the accuracy of the standard deviation calculated by the simulation, which in turns affects the accuracy of the continuous phase space simulation.
Results and Discussion¶
Part I General Analysis¶
From Part I, electrons create four kinds of trajectories: ellipsoids, partial/half ellipsoids, rings, and flat elliptical rings. These shapes are traced by various periodic and aperiodic electron paths, ranging from infinity rings to ellipses to other forms of loops
- From Cycle 1, $N_2 = N_1$: The electrons trace a 3-dimensional ellipsoid, with path densities changing due to differences in $\theta$. The trajectory of the electron is roughly symmetric, which is expected since we have set $N_1 = N_2$.
- From Cycle 2 $N_2 = 5 N_1$: The electrons trace a partial ellipsoid due to the larger $N_2$ force, with the curving of the partial ellipsoid increasing as $\theta$ approaches 90 degrees. This is due to the vertical initial velocity component; as $\theta$ nears 90, the y-velocity increases, allowing the electron to travel further towards $N_1$ before its direction reverses towards $N_2$. In Cycle 2, the force of $N_1$ is still relatively large enough to influence the path of the electron, as seen by the elliptical shape of the trajectory; if there were no $N_1$ influence, the electron should trace a flat elliptical pattern, as seen in Cycle 4.
- From Cycle 3 $N_2 = \frac{1}{5}N_1$: The electrons range between tracing ring-like shapes and partial ellipsoids, depending on $\theta$. As $\theta$ approaches 90 degrees, the electrons have enough vertical velocity to trace a ring-like shape; this occurs due to the low $N_2$ force, allowing the electrons to pass" $N_1$ and $N_2$ before reversing direction towards the other proton. As $\theta$ moves away from 90, the decrease in vertical component velocity gradually increases the thickness of the traced ring until the ring diverges into a partial ellipsoid, as seen in Cycle 2.
- From Cycle 4: $N_2 = 50N_1$: The electron traces a flat elliptical path with drift, creating a rubber-band-like trajectory. This trajectory is different from that of Cycle 2, since the force of $N_2$ is magnitudes larger than $N_1$; now that $N_1$ has a negligible effect on the electron, there is no longer a partial ellipsoid. The electron still experiences drift due to the chosen initial velocity and variable angle; its orbit is non-Keplerian, meaning that its path will never be a two-dimensional ellipse.
Overall, Part I demonstrates that electrons exhibit a wide range of shapes and trajectories in response to varying nucleic charge and angle. This is due to the interactions between $N_2$ and $N_1$ Coulomb force, as well as the initial y and x velocity components from $\theta$.
Part II General Analysis¶
Discrete Phase-Space Diagram¶
Figure 22. Screenshots of three stability regions. From left to right: Low $\frac{N_2}{N_1}$ ratio, curving band, and chaotic region.
From Part II, there are three key regions where stable orbits are achieved:
- Low $\frac{N_2}{N_1}$ ratio: This is the region with the most prevalent stable orbits. When the charge ratio is between 0.01 and ~0.08, nearly all orbits are stable, with some exceptions when the initial velocity angle is between 60 and 70 degrees. Past this charge ratio, most orbits become either unstable due to lack of periodicity or collide into 1 of two protons, with the latter chance increasing as the charge ratio increases. Upon analyzing a stable orbit in this region, we see that the electrons follow an elliptical path with circular drift, creating a partial ellipsoid described in Cycle 2 of the Part I analysis.
- Curving Band: Above the low $\frac{N_2}{N_1}$ ratio region, we see a curving band of stable orbits. Upon analyzing a stable orbit in this region, we see that the electrons follow a figure-eight path, with its trajectory growing each cycle. The electron's path traces a full ellipsoid similar to the ones described in Cycle 1 of the Part I analysis.
- Chaotic Region: In the top right portion of the phase space diagram, there is a chaotic region of stable, unstable, and collision orbits. Upon analyzing a stable orbit in this region, we see that the electron traces a very similar orbit to Trial 2 of Cycle 1, where the electron follows a looping pattern up towards $N_1$ then down towards $N_2$, also tracing out an ellipsoid, but of a higher eccentricity. Interestingly, when we change $N_2$ by $0.005e-19C$, the orbit changes from stable to a collision with the nucleus. This shows how fragile the system in the chaotic region can be; just by varing $N_2$ by $0.03 \%$, the orbit transitions from a stable one to a collision wth the nucleus.
Part III and IV General Analyses¶
Continuous Phase-Space Heat Map¶
From the continuous phase-space heat map, we see all the patterns that the discrete phase-space diagram present with further detail. Three addition key observations are made. Firstly, there is a neon band of quasi-periodic orbits immediately above the lower third region, with a very stark contrast in relative standard deviations between two regions. Secondly, the collision patterns towards the top of the graphs show three swooping curves, along with repeating horizontal patterns. Finally, curves of quasi-periodicity can be seen in the middle of the graph, where streaks of turquoise and purple connect to the lower portion of the graph and transition to less stable orbits (yellow) as charge ratio decreases. A more detailed analysis of the patterns yields the following conclusions:
(1) Periodic and Claw-Mark Collisiond: The periodic and claw-mark collisions seen in the top of the continuous phase space diagrams are due to slight variations in amplitude of electron distances form $N_2$.
(2) Neon bands in lower third region: The abrupt neon bands above the lower region of stable orbits is most likely due to a combination of electron drift and its change in eccentricity in orbits. This in turn creates variations in the electron's orbital periods, contributing to a higher relative standard deviation.
(3) Curves of periodicity: The curves of periodicity in the center of the graph can be attributed to a low drift change of the electron's orbit, characterized by stable figure-eight like orbits
(4) Numerical artifacts: Numerical artifacts in the lower right portion of the phase space graph can be seen, where certain parameter combinations miscount the number of periods due to the quantification of stability through local minimum distances. This affects the accuracy of the simulation and may lead to unrepresentative results coming from our data.
Limitations & Next Steps¶
Technical Limitations¶
- Machine Precision: Since we used Solve_IVP in this project, we will have limitations in machine precision– we are approximating the velocity and position by discretizing our calculations, reducing the accuracy of our simulation. While Solve_IVP is most likely more precise than simply using Euler's method, the function will still have uncertainty.
- CPU Memory & Simplistic Model: Since we are running this project on a limited CPU memory, we cannot run each simulation for a very long period to more accurately determine stability. For context, the phase-space simulation in this project took nearly two hours to run on VS code! This limits the extent and complexity that we can make our simulations.
- Approximating Periodicity: In this project, we approximated periodicity by taking the local minima of the distance between the electron and its original position. However, it is possible that this is not representative of the actual period; the electron may return to its original position, but that point does not mark a period, such as if it does multiple loops around a nucleus before completing a cycle. This is a technical limitation of our simulation, since its approximation of periodicity may not be accurate.
- Other Stability Quantification Limitations: As discussed in Part IV of the analysis, the presence of numerical artifacts is due to limitations in the way this project defined stability. This means that some of the results seen in our phase spaces may not be truly representative of teh electron orbital trajectories.
Next Steps¶
- Improving Phase Space Resolution: In our Part II analysis, we see that our phase space resolution is very low, even after running our phase space over 900 parameter combinations. Running the phase space over more combinations woudl increase the resolution of the graph, allowing us to observe more relevant patterns in the graph.
- Better definition of stability: A more reliable quantifcation of stability could be developed so that there are very few (if not none) numerical artifacts in the simulated results.
- Understanding mechanisms to orbital stability: Further effort could go into understanding why certain combinations of charge ratio and initial velocity angle produce stable orbits, while others do not.
Acknowledgements¶
- I acknowledge the use of Claude AI for helping me develop the functions to extend my simulation code to scan for and plot my 2-D phase space.
- I acknowledge Sumaiya Hussain and Sam Quastel for providing me with preliminary feedback on choosing a phase space for my project.
References¶
Appendix 1: Code validation¶
A1.1: Check for Initial Velocity Angle, $\theta = 0$¶
Our first validation task is extremely brief, where we check that the initial velocity angle is implemented correctly. We specifically test $\theta = 0$ with $N_1 = N_2$, in which the electron's initial velocity will be to the right. Due to this, the forces of $N_2$ and $N_1$ on the electron will be purely horizontal, meaning that we expect the electron's trajectory to oscillate left and right.
def plot_trajectory(sol1, top, T, c):
# Plot y vs x for theta = 5
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(14, 9)
fig.suptitle(f'Electron Orbital Trajectory: N2 = {n2: 0.2g}, Theta = {angle}', fontsize=16, fontweight='bold')
plt.subplots_adjust(top=top)
ax[2].plot(sol1.y[0], sol1.y[1], label = "Trajectory",c = "cornflowerblue")
ax[2].set_aspect("equal")
ax[2].set_title(f"Time Span = {T} s")
ax[2].set_xlabel("x (m)")
ax[2].set_ylabel(" y (m) ")
ax[2].grid(True)
ax[2].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[2].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[2].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[2].legend(loc='upper left', fontsize=9)
# Get the one-third index
idx1 = len(sol1.t) // 3
# Plot only the first one-third of the data
ax[1].plot(sol1.y[0][:idx1], sol1.y[1][:idx1], label="Trajectory", c="cornflowerblue")
ax[1].set_aspect("equal")
ax[1].set_title(f"Time Span = (1/3) * {T} s")
ax[1].set_xlabel("x (m)")
ax[1].set_ylabel(" y (m) ")
ax[1].grid(True)
ax[1].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[1].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[1].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[1].legend(loc='upper left', fontsize=9)
# Get the one-tenth index
idx2 = len(sol1.t) // 8
# Plot only the first one-third of the data
ax[0].plot(sol1.y[0][:idx2], sol1.y[1][:idx2], label="Trajectory", c="cornflowerblue")
ax[0].set_aspect("equal")
ax[0].set_title(f"Time Span = (1/8) * {T}s")
ax[0].set_xlabel("x (m)")
ax[0].set_ylabel(" y (m) ")
ax[0].grid(True)
ax[0].scatter(x1,y1, s = 120, c = "red", zorder = 10, label = "N1", edgecolors='black', linewidth=1)
ax[0].scatter(x2,y2, s = 120, c = "lime", zorder = 10, label = "N2", edgecolors='black', linewidth=1)
ax[0].scatter(r0,0, s = 120, c = "yellow", zorder = 10, label = "Electron", edgecolors='black', linewidth=1)
ax[0].legend(loc='upper left', fontsize=9)
# Find global min and max for x and y
x_min, x_max = 1.2 * sol1.y[0].min(), 1.2 * sol1.y[0].max()
y_min, y_max = 1.2 * sol1.y[1].min(), 1.2 * sol1.y[1].max()
# Apply limits to all subplots
plt.show()
# Parameters to vary
n2 = 1.6e-19 # Nucleus 2 charge
angle = 0 # Launch angle in degrees
T = 1.5e-15 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.4, T, 0.8)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 2.193e-18 ... 1.499e-15 1.500e-15]
y: [[ 5.290e-11 5.756e-11 ... -7.538e-11 -7.696e-11]
[ 0.000e+00 0.000e+00 ... 0.000e+00 0.000e+00]
[ 2.180e+06 2.067e+06 ... -1.615e+06 -1.573e+06]
[ 0.000e+00 0.000e+00 ... 0.000e+00 0.000e+00]]
sol: None
t_events: None
y_events: None
nfev: 4028
njev: 0
nlu: 0
We see that this is indeed the case.
A1.2: Check for Extreme, $\frac{N_2}{N_1}$ Very large¶
Our second validation task will be to check for the electron trajectory when $\frac{N_2}{N_1}$ is very large. When $N_2$ is big, we expect that the orbital trajectory is a very eccentric ellipse where the electron purely orbits $N_2$, since the relative strength of $N_1$ on the electron is negligible. The force on the electron by $N_2$ should be so large that the electron's trajectory is nearly a straight line, indicating an eccentricity ~1. We use $N_2 = 5000N_1$, $\theta = 90$ to check this:
# Parameters to vary
n2 = 5000 * 1.6e-19 # Nucleus 2 charge
angle = 70 # Launch angle in degrees
T = 1.5e-18 # time for simulation to run, s
sol1 = get_trajectory(n2, angle, T)
plot_trajectory(sol1, 1.3, T, 1)
message: The solver successfully reached the end of the integration interval.
success: True
status: 0
t: [ 0.000e+00 7.464e-21 ... 1.496e-18 1.500e-18]
y: [[ 5.290e-11 5.290e-11 ... 5.252e-11 5.248e-11]
[ 0.000e+00 1.117e-14 ... -3.267e-13 -3.675e-13]
[ 7.456e+05 -2.289e+05 ... -9.991e+06 -1.051e+07]
[ 2.049e+06 9.436e+05 ... -1.008e+07 -1.067e+07]]
sol: None
t_events: None
y_events: None
nfev: 3230
njev: 0
nlu: 0
This is indeed what we see, indicating that our simulation is working as planned.
Appendix 2: Reflection questions¶
Reflection 1: Coding Approaches (A)¶
(How well did you apply and extend your coding knowledge in this project? Consider steps you took to make the code more efficient, readable and/or concise. Discuss any new-to-you coding techniques, functions or python packages that you learned how to use. Reflect on any unforeseen coding challenges you faced in completing this project.)
In this project, I extended my coding knowledge by learning to create functions in Python. By creating functions, I made my code more efficient and readable. I also got into the habit of writing code summaries for each of my code blocks, making them more readable. I extended my matplotlib abilities when I learned how to graph subplots and various titles in my Part I analysis. I also learned how to use Solve_IVP, which I had never used before.
Reflection 2: Coding Approaches (B)¶
(Highlight an aspect of your code that you feel you did particularily well. Discuss an aspect of your code that would benefit the most from further effort.)
Relating to reflection 1, I think my code organization was done very well. Especially for Part II, I created many functions needed in my simulation, and successfully integrated them all to generate an informative phase-space diagram. One thing that I could benefit the most from is to make my functions more compact. I currently have several functions that do the same thing with the outputs just being formatted differently, which is not very cofe-efficient. I should improve this by combining such functions into one.
Reflection 3: Simulation phyiscs and investigation (A)¶
(How well did you apply and extend your physical modelling and scientific investigation skills in this project? Consider the phase space you chose to explore and how throroughly you explored it. Consider how you translated physics into code and if appropriate any new physics you learned or developed a more thorough understanding of.)
I extended my physical modelling skills by learning how to make advanced phase-space diagrams. I previously had no idea how to use matplotlib to create heat maps and plots like the one created in Part II. However, with the help of AI and various documentations online, I was able to learn about all the functison matplotlib offers to create informative visuals. I also extended by knowledge about orbits through this project. I am currently taking ASTR 200, and a large part of the course is understanding Keplerian orbits using ellipses and eccentricity. In this project, I extended my knowledge to non-Keplerian orbits, where electrons trace non-elliptical trajectories, and even if they do, the electrons have various amounts of drift in their paths.
Reflection 4: Simulation phyiscs and investigation (B)¶
(Highlight something you feel you did particularily well in terms of the context of your simulation, the physical modelling that you did or the investigation you performed. Discuss an aspect of these dimensions of your project that would benefit the most from further effort.)
I think my Part I analysis was done particularly well because of my thoroughness. The orbits that I chose to show in my Part I analysis each show a distinct characteristics relating back to either nucleic charge or initial velocity angle, helping form an understanding abotu how these two parameters affect orbital stability. I think I was go more in-depth with my Part II analysis though, through investigating specific portions of my phase-space diagram and understanding the differences in orbital trajectories between the three identified regions (See "Limitations and Next Steps").
Reflection 5: Effectiveness of your communication¶
(Highlight something you feel you did particularily well in your visualizations or written communication. Discuss an aspect of your visualizations or written communication that would benefit the most from further effort.)
I think the introduction to my project was done really well! I laid out the groundwork of my project in a straightforward manner, and I think I included everything needed to properly understand my simulations. I could improve my making my code block summaries more informative.
